Add research posts to navbar global search, unify all search behaviour and remove algolia dependency. (#575)

* remove algoliasearch and update search with weight (title=100, tags=50, tldr=25, content=10)

* fix project search and update search with regex \bword\b or \bword

* remove algolia dependencies from package.json, yarn.lock, etc.

* Base search on fuse.js, implement same search in projects/ blog/ and navbar search

* add research to navbar search, improve search functionality, update test.
This commit is contained in:
Stijn Balk
2025-12-02 18:01:23 +01:00
committed by GitHub
parent f408f867d1
commit 6d643a0eeb
10 changed files with 567 additions and 603 deletions

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server"
// These should be the same indexes used in the search route // These should be the same indexes used in the search route
// to ensure consistency // to ensure consistency
const allIndexes = ["blog", "projects"] const allIndexes = ["blog", "projects", "research"]
export async function GET() { export async function GET() {
try { try {

View File

@@ -1,36 +1,10 @@
import algoliasearch from "algoliasearch"
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { getArticles, getProjects } from "@/lib/content"
import { searchArticles, searchProjects } from "@/lib/search"
// Cache search results for better performance export const revalidate = 300 // 5 minutes
export const revalidate = 900 // Revalidate cache after 15 minutes
const appId = const allIndexes = ["blog", "projects", "research"]
process.env.ALGOLIA_APP_ID || process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || ""
const apiKey =
process.env.ALGOLIA_SEARCH_API_KEY ||
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY ||
""
const additionalIndexes = (
process.env.ALGOLIA_ADDITIONAL_INDEXES ||
process.env.NEXT_PUBLIC_ALGOLIA_ADDITIONAL_INDEXES ||
""
)
.split(",")
.map((index) => index.trim())
.filter(Boolean)
const allIndexes = [...additionalIndexes].filter(Boolean) || [
"blog",
"projects",
]
const searchClient = appId && apiKey ? algoliasearch(appId, apiKey) : null
function transformQuery(query: string) {
if (query.toLowerCase().includes("intmax")) {
return query.replace(/intmax/i, "\"intmax\"")
}
return query
}
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
@@ -46,79 +20,104 @@ export async function GET(request: NextRequest) {
}) })
} }
if (!searchClient) { const results = []
return NextResponse.json(
{ // Search articles
error: "Search client not initialized - missing Algolia credentials", if (!indexName || indexName === "blog") {
availableIndexes: [], const articles = getArticles()
}, const matches = searchArticles(articles, query)
{ status: 500 } .slice(0, hitsPerPage)
) .map((article: any) => ({
objectID: article.id,
title: article.title,
content: article.tldr || article.content.slice(0, 200),
url: `/blog/${article.id}`,
}))
if (matches.length > 0) {
results.push({
indexName: "blog",
hits: matches,
})
}
} }
try { // Search projects (applications and devtools only)
const transformedQuery = transformQuery(query) if (!indexName || indexName === "projects") {
const allProjects = getProjects()
// If an index is specified, search only that index const projectsOnly = allProjects.filter(
if (indexName && indexName.trim() !== "") { (p: any) =>
const index = searchClient.initIndex(indexName) p.category?.toLowerCase() === "application" ||
const response = await index.search(transformedQuery, { hitsPerPage }) p.category?.toLowerCase() === "devtools"
return NextResponse.json(
{
hits: response.hits,
status: "success",
availableIndexes: allIndexes,
},
{
headers: {
"Cache-Control":
"public, s-maxage=900, stale-while-revalidate=1800",
},
}
)
}
// Otherwise search across all configured indexes
const searchPromises = allIndexes.map((idxName) => {
return searchClient!
.initIndex(idxName)
.search(transformedQuery, { hitsPerPage })
.then((response) => ({
indexName: idxName,
hits: response.hits,
}))
.catch((err) => {
console.error(`Search error for index ${idxName}:`, err)
return { indexName: idxName, hits: [] }
})
})
const indexResults = await Promise.all(searchPromises)
const nonEmptyResults = indexResults.filter(
(result) => result.hits && result.hits.length > 0
) )
const matches = searchProjects(projectsOnly, query)
.slice(0, hitsPerPage)
.map((project: any) => ({
objectID: project.id,
title: project.name || project.title,
description: project.description || project.tldr,
url: `/projects/${project.id}`,
}))
if (matches.length > 0) {
results.push({
indexName: "projects",
hits: matches,
})
}
}
// Search research (research category only)
if (!indexName || indexName === "research") {
const allProjects = getProjects()
const researchOnly = allProjects.filter(
(p: any) => p.category?.toLowerCase() === "research"
)
const matches = searchProjects(researchOnly, query)
.slice(0, hitsPerPage)
.map((project: any) => ({
objectID: project.id,
title: project.name || project.title,
description: project.description || project.tldr,
url: `/projects/${project.id}`,
}))
if (matches.length > 0) {
results.push({
indexName: "research",
hits: matches,
})
}
}
// If searching specific index, return single index format
if (indexName) {
const indexResult = results.find((r) => r.indexName === indexName)
return NextResponse.json( return NextResponse.json(
{ {
results: nonEmptyResults, hits: indexResult?.hits || [],
status: "success", status: "success",
availableIndexes: allIndexes, availableIndexes: allIndexes,
}, },
{ {
headers: { headers: {
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800", "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
}, },
} }
) )
} catch (error: any) {
console.error("Global search error:", error)
return NextResponse.json(
{
error: error.message || "Search failed",
availableIndexes: [],
},
{ status: 500 }
)
} }
// Return multi-index format
return NextResponse.json(
{
results,
status: "success",
availableIndexes: allIndexes,
},
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
}
)
} }

View File

@@ -158,10 +158,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
{/* External service optimization */} {/* External service optimization */}
<link rel="dns-prefetch" href="https://www.googletagmanager.com" /> <link rel="dns-prefetch" href="https://www.googletagmanager.com" />
{/* Algolia search preconnect for faster search */}
<link rel="preconnect" href="https://latency-dsn.algolia.net" />
<link rel="dns-prefetch" href="https://search.algolia.com" />
</head> </head>
<body suppressHydrationWarning> <body suppressHydrationWarning>
<GlobalProviderLayout> <GlobalProviderLayout>

View File

@@ -3,8 +3,8 @@
import { LABELS } from "@/app/labels" import { LABELS } from "@/app/labels"
import { ProjectCategory, ProjectInterface } from "@/lib/types" import { ProjectCategory, ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils" import { uniq } from "@/lib/utils"
import { searchProjects as fuseSearchProjects } from "@/lib/search"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import Fuse from "fuse.js"
import { import {
createContext, createContext,
useContext, useContext,
@@ -91,74 +91,43 @@ const filterProjects = ({
findAnyMatch?: boolean findAnyMatch?: boolean
projects?: ProjectInterface[] projects?: ProjectInterface[]
}) => { }) => {
const projectList = projectListItems.map((project: any) => ({ let projectList = projectListItems.map((project: any) => ({
...project, ...project,
id: project?.id?.toLowerCase(), id: project?.id?.toLowerCase(),
})) }))
const keys = [ const noActiveFilters =
"name", Object.keys(activeFilters).length === 0 && searchPattern.length === 0
"tldr", if (noActiveFilters) return projectList
"tags.themes",
"tags.keywords",
"tags.builtWith",
"projectStatus",
]
const tagsFiltersQuery: Record<string, string>[] = [] // Apply tag filters first
projectList = projectList.filter((project: any) => {
return Object.entries(activeFilters).every(([filterKey, filterValues]) => {
if (!filterValues || filterValues.length === 0) return true
Object.entries(activeFilters).forEach(([key, values]) => { const projectTags = project.tags?.[filterKey] || []
values.forEach((value) => {
if (!value) return return filterValues.some((filterValue: string) => {
tagsFiltersQuery.push({ if (Array.isArray(projectTags)) {
[`tags.${key}`]: value, return projectTags.some((tag: string) =>
tag.toLowerCase() === filterValue.toLowerCase()
)
}
return false
}) })
}) })
}) })
const noActiveFilters = // Apply text search
tagsFiltersQuery.length === 0 && searchPattern.length === 0 if (searchPattern.length > 0) {
if (noActiveFilters) return projectList projectList = fuseSearchProjects(projectList, searchPattern)
let query: any = {}
if (findAnyMatch) {
query = {
$or: [...tagsFiltersQuery, { name: searchPattern }],
}
} else if (searchPattern?.length === 0) {
query = {
$and: [...tagsFiltersQuery],
}
} else if (tagsFiltersQuery.length === 0) {
query = {
name: searchPattern,
}
} else {
query = {
$and: [
{
$and: [...tagsFiltersQuery],
},
{ name: searchPattern },
],
}
} }
const fuse = new Fuse(projectList, { // Add score for sorting
threshold: 0.3, return projectList.map((project: any) => ({
useExtendedSearch: true, ...project,
includeScore: true, score: 0,
findAllMatches: true,
distance: 200,
keys,
})
const result = fuse.search(query)?.map(({ item, score }) => ({
...item,
score,
})) }))
return result ?? []
} }
const sortProjectByFn = ({ const sortProjectByFn = ({

View File

@@ -6,6 +6,7 @@ import { ArticleInEvidenceCard } from "./article-in-evidance-card"
import { ArticleListCard } from "./article-list-card" import { ArticleListCard } from "./article-list-card"
import { LABELS } from "@/app/labels" import { LABELS } from "@/app/labels"
import { Article, ArticleTag } from "@/lib/content" import { Article, ArticleTag } from "@/lib/content"
import { searchArticles } from "@/lib/search"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { Search as SearchIcon } from "lucide-react" import { Search as SearchIcon } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
@@ -77,16 +78,7 @@ export const ArticlesList: React.FC<ArticlesListProps> = ({
if (searchQuery === "all") { if (searchQuery === "all") {
otherArticles = articles otherArticles = articles
} else if (searchQuery?.length > 0) { } else if (searchQuery?.length > 0) {
otherArticles = articles.filter((article: Article) => { otherArticles = searchArticles(articles, searchQuery)
const title = article.title.toLowerCase()
const content = article.content.toLowerCase()
const tags =
article.tags?.map((tag: ArticleTag) => tag.name.toLowerCase()) ?? []
return (
title.includes(searchQuery.toLowerCase()) ||
tags.some((tag: string) => tag.includes(searchQuery.toLowerCase()))
)
})
} }
const hasTag = tag !== undefined const hasTag = tag !== undefined

123
lib/search.ts Normal file
View File

@@ -0,0 +1,123 @@
import Fuse from "fuse.js"
// Helper to get nested object values (e.g., "tags.name")
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
// Post-filter results with word boundary matching
// This ensures "pir" matches "(pir)", "pir.", "pirate" but NOT "inspired"
function filterByWordBoundary<T>(results: T[], query: string, keys: string[]): T[] {
if (!query.trim()) return results
// Split query into individual words
const words = query.trim().split(/\s+/)
// Create regex patterns for each word (word boundary matching)
const patterns = words.map(word => {
// Escape special regex characters
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// Match at word boundaries (start of word)
return new RegExp(`\\b${escaped}`, 'i')
})
return results.filter(item => {
// Check if ALL query words match at word boundaries in ANY searchable field
return patterns.every(pattern => {
return keys.some(key => {
const value = getNestedValue(item, key)
if (!value) return false
// Handle arrays (like tags)
if (Array.isArray(value)) {
return value.some(v => {
const str = typeof v === 'object' ? JSON.stringify(v) : String(v)
return pattern.test(str)
})
}
return pattern.test(String(value))
})
})
})
}
// Transform query for Fuse.js extended search
// Use include-match operator for fuzzy matching
function preprocessQuery(query: string): string {
const words = query.trim().split(/\s+/)
// Use include-match (') for each word
const patterns = words.map(word => {
// If word already has extended search operators, don't modify it
if (word.startsWith('^') || word.endsWith('$') ||
word.startsWith('=') || word.startsWith("'") ||
word.startsWith('!')) {
return word
}
// Use include-match operator (fuzzy match anywhere)
return `'${word}`
})
return patterns.join(' ')
}
// Search articles with proper field weights
export function searchArticles<T>(articles: T[], query: string): T[] {
if (!query.trim()) return articles
const fuse = new Fuse(articles, {
keys: [
{ name: "title", weight: 1.0 },
{ name: "tags.name", weight: 0.75 },
{ name: "tldr", weight: 0.5 },
{ name: "content", weight: 0.25 },
],
threshold: 0.1,
distance: 200,
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
})
const processedQuery = preprocessQuery(query)
const fuseResults = fuse.search(processedQuery).map((result) => result.item)
// Post-filter with word boundary matching
const searchKeys = ["title", "tags.name", "tldr", "content"]
return filterByWordBoundary(fuseResults, query, searchKeys)
}
// Search projects with proper field weights
export function searchProjects<T>(projects: T[], query: string): T[] {
if (!query.trim()) return projects
const fuse = new Fuse(projects, {
keys: [
{ name: "name", weight: 1.0 },
{ name: "tags.themes", weight: 0.75 },
{ name: "tags.keywords", weight: 0.75 },
{ name: "tags.builtWith", weight: 0.75 },
{ name: "tldr", weight: 0.5 },
{ name: "description", weight: 0.5 },
{ name: "projectStatus", weight: 0.25 },
{ name: "content", weight: 0.25 },
],
threshold: 0.1,
distance: 200,
findAllMatches: true,
includeScore: true,
useExtendedSearch: true,
})
const processedQuery = preprocessQuery(query)
const fuseResults = fuse.search(processedQuery).map((result) => result.item)
// Post-filter with word boundary matching
const searchKeys = [
"name", "tags.themes", "tags.keywords", "tags.builtWith",
"tldr", "description", "projectStatus", "content"
]
return filterByWordBoundary(fuseResults, query, searchKeys)
}

View File

@@ -38,7 +38,6 @@
"@tw-classed/react": "^1.8.0", "@tw-classed/react": "^1.8.0",
"@types/node": "^20.19.0", "@types/node": "^20.19.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"algoliasearch": "^4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"class-variance-authority": "^0.4.0", "class-variance-authority": "^0.4.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@@ -60,7 +59,6 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-cookie": "^7.0.1", "react-cookie": "^7.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-instantsearch-hooks-web": "^6.47.3",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-slick": "^0.30.3", "react-slick": "^0.30.3",
"react-use": "^17.4.0", "react-use": "^17.4.0",

View File

@@ -36,7 +36,7 @@ describe("API Test Coverage", () => {
it("covers all critical API functionality", () => { it("covers all critical API functionality", () => {
const expectedTestCategories = [ const expectedTestCategories = [
"Content APIs (articles, projects)", "Content APIs (articles, projects)",
"Search functionality (Algolia integration)", "Search functionality (local)",
"External integrations (Discord, YouTube)", "External integrations (Discord, YouTube)",
"RSS feed generation", "RSS feed generation",
"Error handling and validation", "Error handling and validation",

View File

@@ -1,33 +1,51 @@
import { GET } from "@/app/api/search/route" import { GET } from "@/app/api/search/route"
import { NextRequest } from "next/server" import { NextRequest } from "next/server"
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" import { describe, it, expect, vi, beforeEach } from "vitest"
// Mock algoliasearch // Mock content functions
const mockSearch = vi.fn() const mockGetArticles = vi.fn()
const mockInitIndex = vi.fn() const mockGetProjects = vi.fn()
vi.mock("algoliasearch", () => ({ vi.mock("@/lib/content", () => ({
default: vi.fn((appId: string, apiKey: string) => getArticles: () => mockGetArticles(),
appId && apiKey getProjects: () => mockGetProjects(),
? {
initIndex: mockInitIndex,
}
: null
),
})) }))
// Mock environment variables // Mock search functions (uses real implementation)
const originalEnv = process.env vi.mock("@/lib/search", async () => {
const actual = await vi.importActual("@/lib/search")
return actual
})
describe("/api/search", () => { describe("/api/search", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
process.env = { ...originalEnv } mockGetArticles.mockReturnValue([
mockInitIndex.mockReturnValue({ search: mockSearch }) {
}) id: "test-article",
title: "Test Article",
afterEach(() => { content: "This is test content",
process.env = originalEnv tldr: "Test summary",
date: "2024-01-01",
tags: [{ id: "test", name: "Test" }],
},
])
mockGetProjects.mockReturnValue([
{
id: "test-project",
name: "Test Project",
title: "Test Project",
description: "Test project description",
category: "application",
},
{
id: "test-research",
name: "Test Research",
title: "Test Research",
description: "Research project description",
category: "research",
},
])
}) })
const createMockRequest = (searchParams: Record<string, string> = {}) => { const createMockRequest = (searchParams: Record<string, string> = {}) => {
@@ -38,19 +56,6 @@ describe("/api/search", () => {
return new NextRequest(url.toString()) return new NextRequest(url.toString())
} }
const mockSearchResults = {
hits: [
{
objectID: "1",
title: "Test Article",
description: "Test description",
url: "/articles/test",
},
],
nbHits: 1,
page: 0,
}
describe("GET /api/search", () => { describe("GET /api/search", () => {
it("returns empty results when query is empty", async () => { it("returns empty results when query is empty", async () => {
const request = createMockRequest({ query: "" }) const request = createMockRequest({ query: "" })
@@ -61,9 +66,8 @@ describe("/api/search", () => {
expect(data).toEqual({ expect(data).toEqual({
results: [], results: [],
status: "empty", status: "empty",
availableIndexes: [], availableIndexes: ["blog", "projects", "research"],
}) })
expect(mockSearch).not.toHaveBeenCalled()
}) })
it("returns empty results when query is whitespace only", async () => { it("returns empty results when query is whitespace only", async () => {
@@ -75,64 +79,304 @@ describe("/api/search", () => {
expect(data).toEqual({ expect(data).toEqual({
results: [], results: [],
status: "empty", status: "empty",
availableIndexes: [], availableIndexes: ["blog", "projects", "research"],
})
expect(mockSearch).not.toHaveBeenCalled()
})
it("returns error when Algolia credentials are missing", async () => {
// Clear environment variables to simulate missing credentials
process.env.ALGOLIA_APP_ID = ""
process.env.ALGOLIA_SEARCH_API_KEY = ""
const request = createMockRequest({ query: "test" })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(500)
expect(data).toEqual({
error: "Search client not initialized - missing Algolia credentials",
availableIndexes: [],
}) })
}) })
it("returns error for search when credentials are invalid", async () => { it("searches articles by title", async () => {
// Test with invalid but present credentials const request = createMockRequest({ query: "test article" })
process.env.ALGOLIA_APP_ID = ""
process.env.ALGOLIA_SEARCH_API_KEY = ""
const request = createMockRequest({
query: "test query",
index: "blog",
})
const response = await GET(request) const response = await GET(request)
const data = await response.json() const data = await response.json()
expect(response.status).toBe(500) expect(response.status).toBe(200)
expect(data).toEqual({ expect(data.status).toBe("success")
error: "Search client not initialized - missing Algolia credentials", expect(data.results.length).toBeGreaterThan(0)
availableIndexes: [], const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult).toBeDefined()
expect(blogResult.hits[0].title).toBe("Test Article")
})
it("searches projects by description", async () => {
const request = createMockRequest({ query: "project", index: "projects" })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.status).toBe("success")
expect(data.hits).toBeDefined()
expect(data.hits.length).toBeGreaterThan(0)
expect(data.hits[0].title).toBe("Test Project")
})
it("searches research index separately", async () => {
const request = createMockRequest({ query: "research", index: "research" })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.status).toBe("success")
expect(data.hits).toBeDefined()
})
it("filters projects by category (application/devtools only)", async () => {
const request = createMockRequest({ query: "test", index: "projects" })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
// Should only return application project, not research
const researchHit = data.hits.find((h: any) => h.objectID === "test-research")
expect(researchHit).toBeUndefined()
})
it("returns no results for non-matching query", async () => {
const request = createMockRequest({ query: "nonexistent" })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.status).toBe("success")
expect(data.results).toHaveLength(0)
})
it("respects hitsPerPage parameter", async () => {
mockGetArticles.mockReturnValue([
{ id: "1", title: "Article 1", content: "test", tldr: "test", date: "2024-01-01", tags: [] },
{ id: "2", title: "Article 2", content: "test", tldr: "test", date: "2024-01-02", tags: [] },
{ id: "3", title: "Article 3", content: "test", tldr: "test", date: "2024-01-03", tags: [] },
])
const request = createMockRequest({ query: "article", hitsPerPage: "2" })
const response = await GET(request)
const data = await response.json()
const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult?.hits.length).toBeLessThanOrEqual(2)
})
// New tests for word boundary matching
describe("Word Boundary Matching", () => {
it("matches words with word boundaries, not partial matches within words", async () => {
mockGetArticles.mockReturnValue([
{
id: "pir-article",
title: "Understanding PIR (Private Information Retrieval)",
content: "PIR is important for privacy",
tldr: "Learn about PIR",
date: "2024-01-01",
tags: [],
},
{
id: "inspired-article",
title: "Inspired by Innovation",
content: "This article was inspired by research",
tldr: "Get inspired",
date: "2024-01-02",
tags: [],
},
])
const request = createMockRequest({ query: "pir" })
const response = await GET(request)
const data = await response.json()
const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult).toBeDefined()
expect(blogResult.hits).toHaveLength(1)
expect(blogResult.hits[0].objectID).toBe("pir-article")
})
it("matches words followed by punctuation at boundaries", async () => {
mockGetProjects.mockReturnValue([
{
id: "semaphore",
name: "Semaphore",
title: "Semaphore Protocol",
description: "Privacy protocol (Semaphore) for anonymous signaling.",
category: "application",
},
])
const request = createMockRequest({ query: "semaphore", index: "projects" })
const response = await GET(request)
const data = await response.json()
expect(data.hits.length).toBeGreaterThan(0)
expect(data.hits[0].objectID).toBe("semaphore")
})
it("performs case-insensitive word boundary matching", async () => {
mockGetArticles.mockReturnValue([
{
id: "snark-article",
title: "SNARK Technology Overview",
content: "SNARKs are powerful cryptographic proofs",
tldr: "Learn about SNARKs",
date: "2024-01-01",
tags: [],
},
])
const request = createMockRequest({ query: "snark" })
const response = await GET(request)
const data = await response.json()
const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult).toBeDefined()
expect(blogResult.hits).toHaveLength(1)
expect(blogResult.hits[0].title).toBe("SNARK Technology Overview")
})
it("requires all words in multi-word query to match at word boundaries", async () => {
mockGetArticles.mockReturnValue([
{
id: "crypto-zk",
title: "Zero Knowledge Cryptography",
content: "ZK proofs are used in crypto applications",
tldr: "ZK and crypto together",
date: "2024-01-01",
tags: [],
},
{
id: "encryption-only",
title: "Cryptographic Methods",
content: "Various encryption techniques without zero knowledge",
tldr: "Encryption techniques",
date: "2024-01-02",
tags: [],
},
])
const request = createMockRequest({ query: "crypto zero" })
const response = await GET(request)
const data = await response.json()
const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult).toBeDefined()
expect(blogResult.hits).toHaveLength(1)
expect(blogResult.hits[0].objectID).toBe("crypto-zk")
})
it("returns no results when fuzzy matches don't satisfy word boundaries", async () => {
mockGetArticles.mockReturnValue([
{
id: "inspiration",
title: "Inspiration for Developers",
content: "Get inspired by these innovative ideas",
tldr: "Developer inspiration",
date: "2024-01-01",
tags: [],
},
])
// "pir" should not match "inspiration" due to word boundary filtering
const request = createMockRequest({ query: "pir" })
const response = await GET(request)
const data = await response.json()
expect(data.status).toBe("success")
expect(data.results).toHaveLength(0)
}) })
}) })
it("handles search errors gracefully for specific index", async () => { // New tests for tag matching
const error = new Error("Algolia search failed") describe("Tag Search with Word Boundaries", () => {
it("searches tags with word boundary matching", async () => {
mockGetArticles.mockReturnValue([
{
id: "privacy-article",
title: "Privacy Technologies (PIR)",
content: "Various privacy tools including PIR",
tldr: "Privacy overview with PIR",
tags: [{ id: "pir", name: "PIR" }],
date: "2024-01-01",
},
{
id: "inspiration-article",
title: "Inspired Ideas",
content: "Inspiration for developers",
tldr: "Get inspired",
tags: [{ id: "inspiration", name: "Inspiration" }],
date: "2024-01-02",
},
])
// Set up valid credentials but mock will reject const request = createMockRequest({ query: "pir" })
process.env.ALGOLIA_APP_ID = "" const response = await GET(request)
process.env.ALGOLIA_SEARCH_API_KEY = "" const data = await response.json()
const request = createMockRequest({ const blogResult = data.results.find((r: any) => r.indexName === "blog")
query: "test", expect(blogResult).toBeDefined()
index: "blog", expect(blogResult.hits).toHaveLength(1)
expect(blogResult.hits[0].objectID).toBe("privacy-article")
}) })
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(500) it("matches tags across multiple tag fields in projects", async () => {
expect(data).toEqual({ mockGetProjects.mockReturnValue([
error: "Search client not initialized - missing Algolia credentials", {
availableIndexes: [], id: "zk-project",
name: "ZK Application",
title: "ZK Application",
description: "Application using zero knowledge",
category: "application",
tags: {
themes: ["privacy"],
keywords: ["zk", "cryptography"],
builtWith: ["circom"],
},
},
])
const request = createMockRequest({ query: "zk", index: "projects" })
const response = await GET(request)
const data = await response.json()
expect(data.hits).toHaveLength(1)
expect(data.hits[0].objectID).toBe("zk-project")
})
})
// New tests for fuzzy matching
describe("Fuzzy Matching", () => {
it("performs fuzzy matching with low threshold for typos", async () => {
mockGetProjects.mockReturnValue([
{
id: "semaphore",
name: "Semaphore",
title: "Semaphore",
description: "Privacy signaling protocol",
category: "application",
},
])
// "semaphor" (missing 'e') should match "Semaphore" with fuzzy matching
const request = createMockRequest({ query: "semaphor", index: "projects" })
const response = await GET(request)
const data = await response.json()
expect(data.hits.length).toBeGreaterThan(0)
expect(data.hits[0].objectID).toBe("semaphore")
})
it("handles special characters in search queries", async () => {
mockGetArticles.mockReturnValue([
{
id: "regex-article",
title: "Understanding Regular Expressions (regex)",
content: "Learn about regex patterns",
tldr: "Regex tutorial",
date: "2024-01-01",
tags: [],
},
])
const request = createMockRequest({ query: "regex" })
const response = await GET(request)
const data = await response.json()
const blogResult = data.results.find((r: any) => r.indexName === "blog")
expect(blogResult).toBeDefined()
expect(blogResult.hits.length).toBeGreaterThan(0)
}) })
}) })
}) })

361
yarn.lock
View File

@@ -12,181 +12,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@algolia/cache-browser-local-storage@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/cache-browser-local-storage@npm:4.25.2"
dependencies:
"@algolia/cache-common": "npm:4.25.2"
checksum: 10/ca0f39001e1d8d9b42adea349baadf1ff9f2ff43e87f4bc85928c0298cc533c1b061f393e1df40a87dbc365e4231dd85a6d8867fbdbd7e6de74c4a79745f307d
languageName: node
linkType: hard
"@algolia/cache-common@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/cache-common@npm:4.25.2"
checksum: 10/706dca5d9c570490b4760646a94d5ed7b603cc846ae3b62c42fc5e7958cdd74b7bd37506be62bf4b97f4b6551526f7a64597c3240fe2c5e7477a8ece986c8d12
languageName: node
linkType: hard
"@algolia/cache-in-memory@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/cache-in-memory@npm:4.25.2"
dependencies:
"@algolia/cache-common": "npm:4.25.2"
checksum: 10/ffd66ff76cba41a1dd26e3575902881e7a5f685018c5d5b6ec2a972c682a56940b593ac3d33d59aac296db884328f0bd19a16e83281c69e3582ab121ed759d56
languageName: node
linkType: hard
"@algolia/client-account@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/client-account@npm:4.25.2"
dependencies:
"@algolia/client-common": "npm:4.25.2"
"@algolia/client-search": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/a39e1be468aef039093f604872afee2cdf8ba17b1b6ed46fe4454a199b147c54a6daccef4efa830d1536385a3f0f5999bb37c52c9b4e09b943117b82d9bd5b3a
languageName: node
linkType: hard
"@algolia/client-analytics@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/client-analytics@npm:4.25.2"
dependencies:
"@algolia/client-common": "npm:4.25.2"
"@algolia/client-search": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/9954f32826d47cd99cb3bd34c7164d752ae5ff895e53ad5fddb4ac1949a98718ef0bb9c9e6bb0df1713438981d9bf038257137ab62df1a3bd72d6836f681b80e
languageName: node
linkType: hard
"@algolia/client-common@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/client-common@npm:4.25.2"
dependencies:
"@algolia/requester-common": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/b88e2abb9dede8fd457471f33f9592b2c0ad3866a95773a3075f8dd457ab400e336d68015f51f826a47aa31662750886ec579a20d45b8f90078269bcf0b5c0c3
languageName: node
linkType: hard
"@algolia/client-personalization@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/client-personalization@npm:4.25.2"
dependencies:
"@algolia/client-common": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/5c2547f40de5ce65fb25685ccee830c98c8d868c4eba3d49710238fdd9b8b6880eaad51fe32831cba1ee04fd614efe8aa50acce492095c09e6d20c91d0595ae9
languageName: node
linkType: hard
"@algolia/client-search@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/client-search@npm:4.25.2"
dependencies:
"@algolia/client-common": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/90a4df2ed4da8d4699801b74aaa1351fae098172cd5905afecd1b645294e7083080fff97722878a41135dc8a692623bc3ad69540d438150ed23ac534d7e1e63d
languageName: node
linkType: hard
"@algolia/events@npm:^4.0.1":
version: 4.0.1
resolution: "@algolia/events@npm:4.0.1"
checksum: 10/98d239899a9dac9398f751221369523f2d7706fc4b3bc3167b66a101773d57380fc52733467c0a12be36bce969577fd4010d6ccbd08c410f9c7adc088dadf4c6
languageName: node
linkType: hard
"@algolia/logger-common@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/logger-common@npm:4.25.2"
checksum: 10/56676de8131841cc966cd60f7804ff61d2266f56c1f8045ccb99680ce5b28eeecbb3db42b40add8750c17ac6bd3b0cee0eaa1f9ee38af38b17c4553b40bf2f22
languageName: node
linkType: hard
"@algolia/logger-console@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/logger-console@npm:4.25.2"
dependencies:
"@algolia/logger-common": "npm:4.25.2"
checksum: 10/f593e31479c41373112f89d7e38a0a2d28312b2c885f0e7286dc39685842098c3ac9a22ebbd9c6b965be09077c5184830a50405c3f32150ee961d9b60f86f564
languageName: node
linkType: hard
"@algolia/recommend@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/recommend@npm:4.25.2"
dependencies:
"@algolia/cache-browser-local-storage": "npm:4.25.2"
"@algolia/cache-common": "npm:4.25.2"
"@algolia/cache-in-memory": "npm:4.25.2"
"@algolia/client-common": "npm:4.25.2"
"@algolia/client-search": "npm:4.25.2"
"@algolia/logger-common": "npm:4.25.2"
"@algolia/logger-console": "npm:4.25.2"
"@algolia/requester-browser-xhr": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
"@algolia/requester-node-http": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/79d75c34bd24ac73a957a2450efcdc1024bae61c3394530f3c815d93b3a5aa3fefbec1ec5de6abd1b1d7454d75e3f8d5026d98f655cdb220898e69e7f08391e3
languageName: node
linkType: hard
"@algolia/requester-browser-xhr@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/requester-browser-xhr@npm:4.25.2"
dependencies:
"@algolia/requester-common": "npm:4.25.2"
checksum: 10/62f2caded45a1af6f4aa4bde925565f21d037010ff89bab0bb58289bda9bd0ecfa9dd5a451b74ef059970f280cadca98a8b294bd055f70fa75fd5034507d5c19
languageName: node
linkType: hard
"@algolia/requester-common@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/requester-common@npm:4.25.2"
checksum: 10/68ae6e4ff01f67807e1b61ea37433e4d0a39fdfbd1da5fcc35e3180757fd272cea7fd07bebe7510d7b7fcfd6dc1992535cb70595829b7e1cd12d516c2df523ff
languageName: node
linkType: hard
"@algolia/requester-node-http@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/requester-node-http@npm:4.25.2"
dependencies:
"@algolia/requester-common": "npm:4.25.2"
checksum: 10/31a62aae0c041f49b2b5bf4dd794d88f6f5b356902b3a264061c2dc3a5273f6edfc69a06a5c40c0cdc6a25e4bbc66e0eb9f37ac3e65593a5f53293bab0f96560
languageName: node
linkType: hard
"@algolia/transporter@npm:4.25.2":
version: 4.25.2
resolution: "@algolia/transporter@npm:4.25.2"
dependencies:
"@algolia/cache-common": "npm:4.25.2"
"@algolia/logger-common": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
checksum: 10/4ad449c56b142806577e885edfbeb11c0219ce08cc8128c2d5283d72a00ad4c37173784e098b4c8aa6052667569f8886d6d096136cf9e526779551c130e82daf
languageName: node
linkType: hard
"@algolia/ui-components-highlight-vdom@npm:^1.2.1":
version: 1.2.3
resolution: "@algolia/ui-components-highlight-vdom@npm:1.2.3"
dependencies:
"@algolia/ui-components-shared": "npm:1.2.3"
"@babel/runtime": "npm:^7.0.0"
checksum: 10/20b3ac2c8dd34f50a77c3872f23b315cf31cb76f862a29de3df4f733a01b548fb78b4441bea0988ded8b3222ab76906317cf2fd30e359fdf6e5defd7337ab743
languageName: node
linkType: hard
"@algolia/ui-components-shared@npm:1.2.3, @algolia/ui-components-shared@npm:^1.2.1":
version: 1.2.3
resolution: "@algolia/ui-components-shared@npm:1.2.3"
checksum: 10/bebed83507b1084d443dba435869348f8370c111b0fac224969f4dac197d157367c193f3a1ba36c8d2f0818028586c6a7f831cfb7990bff7ef0e5e3aa347bb78
languageName: node
linkType: hard
"@alloc/quick-lru@npm:^5.2.0": "@alloc/quick-lru@npm:^5.2.0":
version: 5.2.0 version: 5.2.0
resolution: "@alloc/quick-lru@npm:5.2.0" resolution: "@alloc/quick-lru@npm:5.2.0"
@@ -385,7 +210,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5": "@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5":
version: 7.28.2 version: 7.28.2
resolution: "@babel/runtime@npm:7.28.2" resolution: "@babel/runtime@npm:7.28.2"
checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a
@@ -2425,13 +2250,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/dom-speech-recognition@npm:^0.0.1":
version: 0.0.1
resolution: "@types/dom-speech-recognition@npm:0.0.1"
checksum: 10/9ac74dbfb1d28e90a052db858c9298f9987717674537f3f6eb86baf85bd691cb061b7f21bd43b3dd8d3fd101ad190fe77e5b4e1cfa15fb5ad12431ec29e32490
languageName: node
linkType: hard
"@types/estree-jsx@npm:^1.0.0": "@types/estree-jsx@npm:^1.0.0":
version: 1.0.5 version: 1.0.5
resolution: "@types/estree-jsx@npm:1.0.5" resolution: "@types/estree-jsx@npm:1.0.5"
@@ -2448,13 +2266,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/google.maps@npm:^3.45.3":
version: 3.58.1
resolution: "@types/google.maps@npm:3.58.1"
checksum: 10/3d5aaa901c0b5dcce45dc9f667912c04b99be0b4a8b541b5120b677697d17116684fddb457bea4955142755c9089993ea4b48b30705283c16935473b1818ecd1
languageName: node
linkType: hard
"@types/hast@npm:^2.0.0": "@types/hast@npm:^2.0.0":
version: 2.3.10 version: 2.3.10
resolution: "@types/hast@npm:2.3.10" resolution: "@types/hast@npm:2.3.10"
@@ -2473,13 +2284,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/hogan.js@npm:^3.0.0":
version: 3.0.5
resolution: "@types/hogan.js@npm:3.0.5"
checksum: 10/a2cc95b1a94bd321aa2fe0303005703a7e801cf463ee7b3ab5e2fae101ef426ace87bf9554bb995c8d3c60c2612b657d765d20d96faae3af03bd0e3a55357aba
languageName: node
linkType: hard
"@types/hoist-non-react-statics@npm:^3.3.5": "@types/hoist-non-react-statics@npm:^3.3.5":
version: 3.3.7 version: 3.3.7
resolution: "@types/hoist-non-react-statics@npm:3.3.7" resolution: "@types/hoist-non-react-statics@npm:3.3.7"
@@ -2592,13 +2396,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/qs@npm:^6.5.3":
version: 6.14.0
resolution: "@types/qs@npm:6.14.0"
checksum: 10/1909205514d22b3cbc7c2314e2bd8056d5f05dfb21cf4377f0730ee5e338ea19957c41735d5e4806c746176563f50005bbab602d8358432e25d900bdf4970826
languageName: node
linkType: hard
"@types/react-dom@npm:^18.2.4": "@types/react-dom@npm:^18.2.4":
version: 18.3.7 version: 18.3.7
resolution: "@types/react-dom@npm:18.3.7" resolution: "@types/react-dom@npm:18.3.7"
@@ -3171,13 +2968,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"abbrev@npm:1":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243
languageName: node
linkType: hard
"abbrev@npm:^3.0.0": "abbrev@npm:^3.0.0":
version: 3.0.1 version: 3.0.1
resolution: "abbrev@npm:3.0.1" resolution: "abbrev@npm:3.0.1"
@@ -3240,40 +3030,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"algoliasearch-helper@npm:3.14.0":
version: 3.14.0
resolution: "algoliasearch-helper@npm:3.14.0"
dependencies:
"@algolia/events": "npm:^4.0.1"
peerDependencies:
algoliasearch: ">= 3.1 < 6"
checksum: 10/5a3e1fda05a1688153577a0377fd38aaf84a3c095c63fc14877acd34449d41c274f6fb38dc8c6041e1f5c198425cd24653f9bac0f1e18a351ba7a3b500641aac
languageName: node
linkType: hard
"algoliasearch@npm:^4":
version: 4.25.2
resolution: "algoliasearch@npm:4.25.2"
dependencies:
"@algolia/cache-browser-local-storage": "npm:4.25.2"
"@algolia/cache-common": "npm:4.25.2"
"@algolia/cache-in-memory": "npm:4.25.2"
"@algolia/client-account": "npm:4.25.2"
"@algolia/client-analytics": "npm:4.25.2"
"@algolia/client-common": "npm:4.25.2"
"@algolia/client-personalization": "npm:4.25.2"
"@algolia/client-search": "npm:4.25.2"
"@algolia/logger-common": "npm:4.25.2"
"@algolia/logger-console": "npm:4.25.2"
"@algolia/recommend": "npm:4.25.2"
"@algolia/requester-browser-xhr": "npm:4.25.2"
"@algolia/requester-common": "npm:4.25.2"
"@algolia/requester-node-http": "npm:4.25.2"
"@algolia/transporter": "npm:4.25.2"
checksum: 10/d8d13ff04db7bceb6b5d01a0736f5b37538941188b6866f61457e78326d4f6e45a08c22478c8f86e8b178f98d94c07d5aaed74a4a435e841d7190650ebec4167
languageName: node
linkType: hard
"ansi-escapes@npm:^7.0.0": "ansi-escapes@npm:^7.0.0":
version: 7.0.0 version: 7.0.0
resolution: "ansi-escapes@npm:7.0.0" resolution: "ansi-escapes@npm:7.0.0"
@@ -5972,18 +5728,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"hogan.js@npm:^3.0.2":
version: 3.0.2
resolution: "hogan.js@npm:3.0.2"
dependencies:
mkdirp: "npm:0.3.0"
nopt: "npm:1.0.10"
bin:
hulk: ./bin/hulk
checksum: 10/385784c5e61dafe019b01bad57b52cac27bc7509c1ad213dcbdd4bc39b001ef25f5af8af03dbb4e9885eaa2dad5462c87dd668e1eb6697fe71c1c8af6887b09e
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2" resolution: "hoist-non-react-statics@npm:3.3.2"
@@ -5993,13 +5737,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"htm@npm:^3.0.0":
version: 3.1.1
resolution: "htm@npm:3.1.1"
checksum: 10/cb862dc5c9eac532937af7a9e26edd1e0e7939fc78a06efde4ae10b3a145f9506e644ff084c871dd808c315342b56fd0baa174a2a2cdf6071a4130ee0abee9e0
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0": "html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0" resolution: "html-encoding-sniffer@npm:4.0.0"
@@ -6199,29 +5936,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"instantsearch.js@npm:4.56.8":
version: 4.56.8
resolution: "instantsearch.js@npm:4.56.8"
dependencies:
"@algolia/events": "npm:^4.0.1"
"@algolia/ui-components-highlight-vdom": "npm:^1.2.1"
"@algolia/ui-components-shared": "npm:^1.2.1"
"@types/dom-speech-recognition": "npm:^0.0.1"
"@types/google.maps": "npm:^3.45.3"
"@types/hogan.js": "npm:^3.0.0"
"@types/qs": "npm:^6.5.3"
algoliasearch-helper: "npm:3.14.0"
hogan.js: "npm:^3.0.2"
htm: "npm:^3.0.0"
preact: "npm:^10.10.0"
qs: "npm:^6.5.1 < 6.10"
search-insights: "npm:^2.6.0"
peerDependencies:
algoliasearch: ">= 3.1 < 6"
checksum: 10/ec7ee1fe8f54c92e27ffed1e4eecef14efbe36e2faeceb73c575e8051ba2f466a0d687a05c271e2874acc9eb583ee39f19535487c4077d5875985078517429b4
languageName: node
linkType: hard
"internal-slot@npm:^1.1.0": "internal-slot@npm:^1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "internal-slot@npm:1.1.0" resolution: "internal-slot@npm:1.1.0"
@@ -8346,13 +8060,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mkdirp@npm:0.3.0":
version: 0.3.0
resolution: "mkdirp@npm:0.3.0"
checksum: 10/51b0010427561f044f3c2f453163c9e9452c4a26643d63defd8313674e314cde3866019f19e8e9fc7eefcff4c73666fa7ae8e4676b85bc15b5fddd850bffbed1
languageName: node
linkType: hard
"mkdirp@npm:^3.0.1": "mkdirp@npm:^3.0.1":
version: 3.0.1 version: 3.0.1
resolution: "mkdirp@npm:3.0.1" resolution: "mkdirp@npm:3.0.1"
@@ -8487,7 +8194,6 @@ __metadata:
"@vitejs/plugin-react": "npm:^4.7.0" "@vitejs/plugin-react": "npm:^4.7.0"
"@vitest/coverage-v8": "npm:3.2.4" "@vitest/coverage-v8": "npm:3.2.4"
"@vitest/ui": "npm:^3.2.4" "@vitest/ui": "npm:^3.2.4"
algoliasearch: "npm:^4"
autoprefixer: "npm:^10.4.14" autoprefixer: "npm:^10.4.14"
class-variance-authority: "npm:^0.4.0" class-variance-authority: "npm:^0.4.0"
clsx: "npm:^1.2.1" clsx: "npm:^1.2.1"
@@ -8520,7 +8226,6 @@ __metadata:
react: "npm:^18.2.0" react: "npm:^18.2.0"
react-cookie: "npm:^7.0.1" react-cookie: "npm:^7.0.1"
react-dom: "npm:^18.2.0" react-dom: "npm:^18.2.0"
react-instantsearch-hooks-web: "npm:^6.47.3"
react-markdown: "npm:^8.0.7" react-markdown: "npm:^8.0.7"
react-slick: "npm:^0.30.3" react-slick: "npm:^0.30.3"
react-use: "npm:^17.4.0" react-use: "npm:^17.4.0"
@@ -8649,17 +8354,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nopt@npm:1.0.10":
version: 1.0.10
resolution: "nopt@npm:1.0.10"
dependencies:
abbrev: "npm:1"
bin:
nopt: ./bin/nopt.js
checksum: 10/4f01ad1e144883a190d70bd6003f26e2f3a899230fe1b0f3310e43779c61cab5ae0063a9209912cd52fc4c552b266b38173853aa9abe27ecb04acbdfdca2e9fc
languageName: node
linkType: hard
"nopt@npm:^8.0.0": "nopt@npm:^8.0.0":
version: 8.1.0 version: 8.1.0
resolution: "nopt@npm:8.1.0" resolution: "nopt@npm:8.1.0"
@@ -9123,13 +8817,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"preact@npm:^10.10.0":
version: 10.27.0
resolution: "preact@npm:10.27.0"
checksum: 10/440685d450349acb5802fbdba33b53b727e2aeb57a23bfa28b597a97b2e1f046f2e1abc0bd51662968ff3a45ebbcc33894b929fdc412b1644651669e2860dfac
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1": "prelude-ls@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "prelude-ls@npm:1.2.1" resolution: "prelude-ls@npm:1.2.1"
@@ -9227,13 +8914,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"qs@npm:^6.5.1 < 6.10":
version: 6.9.7
resolution: "qs@npm:6.9.7"
checksum: 10/fb364b54bf4f092a095554968f5abf06036cfe359c9aba258a81b0c0366f625a46098fe1224b2a71ee2f88642470af391c7a8a1496508eca29c37093293f91a9
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2": "queue-microtask@npm:^1.2.2":
version: 1.2.3 version: 1.2.3
resolution: "queue-microtask@npm:1.2.3" resolution: "queue-microtask@npm:1.2.3"
@@ -9266,36 +8946,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-instantsearch-hooks-web@npm:^6.47.3":
version: 6.47.3
resolution: "react-instantsearch-hooks-web@npm:6.47.3"
dependencies:
"@babel/runtime": "npm:^7.1.2"
instantsearch.js: "npm:4.56.8"
react-instantsearch-hooks: "npm:6.47.3"
peerDependencies:
algoliasearch: ">= 3.1 < 5"
react: ">= 16.8.0 < 19"
react-dom: ">= 16.8.0 < 19"
checksum: 10/366f9088030c538a29ccf7f7860a652d962179a6b7b517e83d4a47569d0b3e53f077ddbe9549059b3bb6c3b86041cb6aa6fd497aec779c3064790755d4fa2d84
languageName: node
linkType: hard
"react-instantsearch-hooks@npm:6.47.3":
version: 6.47.3
resolution: "react-instantsearch-hooks@npm:6.47.3"
dependencies:
"@babel/runtime": "npm:^7.1.2"
algoliasearch-helper: "npm:3.14.0"
instantsearch.js: "npm:4.56.8"
use-sync-external-store: "npm:^1.0.0"
peerDependencies:
algoliasearch: ">= 3.1 < 5"
react: ">= 16.8.0 < 19"
checksum: 10/65b4ff9c5aa9262df1d477d91d7bdb2e99fae387576bc2f144fbd42d5bfb8b8b85c7dffad359d78c122149101f992a520da44ae9af5d330ba614bb295836535b
languageName: node
linkType: hard
"react-is@npm:^16.13.1, react-is@npm:^16.7.0": "react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1 version: 16.13.1
resolution: "react-is@npm:16.13.1" resolution: "react-is@npm:16.13.1"
@@ -10014,13 +9664,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"search-insights@npm:^2.6.0":
version: 2.17.3
resolution: "search-insights@npm:2.17.3"
checksum: 10/7f2d7c5d317d84bb9bb745f3e5cd411c206fb72e453331515712bda855e3ee8af4b767231a4bc25693eadd34e2ffd58b6eebb7c407fc17eeb2932cc997442dff
languageName: node
linkType: hard
"section-matter@npm:^1.0.0": "section-matter@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "section-matter@npm:1.0.0" resolution: "section-matter@npm:1.0.0"
@@ -11602,7 +11245,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.2": "use-sync-external-store@npm:^1.2.2":
version: 1.5.0 version: 1.5.0
resolution: "use-sync-external-store@npm:1.5.0" resolution: "use-sync-external-store@npm:1.5.0"
peerDependencies: peerDependencies: