diff --git a/app/api/search/indexes/route.ts b/app/api/search/indexes/route.ts
index 9222e50e..4febe72f 100644
--- a/app/api/search/indexes/route.ts
+++ b/app/api/search/indexes/route.ts
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server"
// These should be the same indexes used in the search route
// to ensure consistency
-const allIndexes = ["blog", "projects"]
+const allIndexes = ["blog", "projects", "research"]
export async function GET() {
try {
diff --git a/app/api/search/route.ts b/app/api/search/route.ts
index 34511481..53ecf931 100644
--- a/app/api/search/route.ts
+++ b/app/api/search/route.ts
@@ -1,36 +1,10 @@
-import algoliasearch from "algoliasearch"
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 = 900 // Revalidate cache after 15 minutes
+export const revalidate = 300 // 5 minutes
-const appId =
- 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
-}
+const allIndexes = ["blog", "projects", "research"]
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
@@ -46,79 +20,104 @@ export async function GET(request: NextRequest) {
})
}
- if (!searchClient) {
- return NextResponse.json(
- {
- error: "Search client not initialized - missing Algolia credentials",
- availableIndexes: [],
- },
- { status: 500 }
- )
- }
-
- try {
- const transformedQuery = transformQuery(query)
+ const results = []
- // If an index is specified, search only that index
- if (indexName && indexName.trim() !== "") {
- const index = searchClient.initIndex(indexName)
- const response = await index.search(transformedQuery, { hitsPerPage })
+ // Search articles
+ if (!indexName || indexName === "blog") {
+ const articles = getArticles()
+ const matches = searchArticles(articles, query)
+ .slice(0, hitsPerPage)
+ .map((article: any) => ({
+ objectID: article.id,
+ title: article.title,
+ content: article.tldr || article.content.slice(0, 200),
+ url: `/blog/${article.id}`,
+ }))
- return NextResponse.json(
- {
- hits: response.hits,
- status: "success",
- availableIndexes: allIndexes,
- },
- {
- headers: {
- "Cache-Control":
- "public, s-maxage=900, stale-while-revalidate=1800",
- },
- }
- )
+ if (matches.length > 0) {
+ results.push({
+ indexName: "blog",
+ hits: matches,
+ })
}
+ }
- // 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: [] }
- })
- })
+ // Search projects (applications and devtools only)
+ if (!indexName || indexName === "projects") {
+ const allProjects = getProjects()
+ const projectsOnly = allProjects.filter(
+ (p: any) =>
+ p.category?.toLowerCase() === "application" ||
+ p.category?.toLowerCase() === "devtools"
+ )
+ 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}`,
+ }))
- const indexResults = await Promise.all(searchPromises)
- const nonEmptyResults = indexResults.filter(
- (result) => result.hits && result.hits.length > 0
+ 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(
{
- results: nonEmptyResults,
+ hits: indexResult?.hits || [],
status: "success",
availableIndexes: allIndexes,
},
{
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",
+ },
+ }
+ )
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 0fca8fbf..371207c3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -158,10 +158,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
{/* External service optimization */}
-
- {/* Algolia search preconnect for faster search */}
-
-
diff --git a/app/providers/ProjectsProvider.tsx b/app/providers/ProjectsProvider.tsx
index 1661f0cd..076d8515 100644
--- a/app/providers/ProjectsProvider.tsx
+++ b/app/providers/ProjectsProvider.tsx
@@ -3,8 +3,8 @@
import { LABELS } from "@/app/labels"
import { ProjectCategory, ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils"
+import { searchProjects as fuseSearchProjects } from "@/lib/search"
import { useQuery } from "@tanstack/react-query"
-import Fuse from "fuse.js"
import {
createContext,
useContext,
@@ -91,74 +91,43 @@ const filterProjects = ({
findAnyMatch?: boolean
projects?: ProjectInterface[]
}) => {
- const projectList = projectListItems.map((project: any) => ({
+ let projectList = projectListItems.map((project: any) => ({
...project,
id: project?.id?.toLowerCase(),
}))
- const keys = [
- "name",
- "tldr",
- "tags.themes",
- "tags.keywords",
- "tags.builtWith",
- "projectStatus",
- ]
+ const noActiveFilters =
+ Object.keys(activeFilters).length === 0 && searchPattern.length === 0
+ if (noActiveFilters) return projectList
+
+ // Apply tag filters first
+ projectList = projectList.filter((project: any) => {
+ return Object.entries(activeFilters).every(([filterKey, filterValues]) => {
+ if (!filterValues || filterValues.length === 0) return true
- const tagsFiltersQuery: Record[] = []
+ const projectTags = project.tags?.[filterKey] || []
- Object.entries(activeFilters).forEach(([key, values]) => {
- values.forEach((value) => {
- if (!value) return
- tagsFiltersQuery.push({
- [`tags.${key}`]: value,
+ return filterValues.some((filterValue: string) => {
+ if (Array.isArray(projectTags)) {
+ return projectTags.some((tag: string) =>
+ tag.toLowerCase() === filterValue.toLowerCase()
+ )
+ }
+ return false
})
})
})
- const noActiveFilters =
- tagsFiltersQuery.length === 0 && searchPattern.length === 0
- if (noActiveFilters) return projectList
-
- 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 },
- ],
- }
+ // Apply text search
+ if (searchPattern.length > 0) {
+ projectList = fuseSearchProjects(projectList, searchPattern)
}
- const fuse = new Fuse(projectList, {
- threshold: 0.3,
- useExtendedSearch: true,
- includeScore: true,
- findAllMatches: true,
- distance: 200,
- keys,
- })
-
- const result = fuse.search(query)?.map(({ item, score }) => ({
- ...item,
- score,
+ // Add score for sorting
+ return projectList.map((project: any) => ({
+ ...project,
+ score: 0,
}))
- return result ?? []
}
const sortProjectByFn = ({
diff --git a/components/blog/articles-list.tsx b/components/blog/articles-list.tsx
index c08b7f25..ed338ded 100644
--- a/components/blog/articles-list.tsx
+++ b/components/blog/articles-list.tsx
@@ -6,6 +6,7 @@ import { ArticleInEvidenceCard } from "./article-in-evidance-card"
import { ArticleListCard } from "./article-list-card"
import { LABELS } from "@/app/labels"
import { Article, ArticleTag } from "@/lib/content"
+import { searchArticles } from "@/lib/search"
import { useQuery } from "@tanstack/react-query"
import { Search as SearchIcon } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
@@ -77,16 +78,7 @@ export const ArticlesList: React.FC = ({
if (searchQuery === "all") {
otherArticles = articles
} else if (searchQuery?.length > 0) {
- otherArticles = articles.filter((article: Article) => {
- 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()))
- )
- })
+ otherArticles = searchArticles(articles, searchQuery)
}
const hasTag = tag !== undefined
diff --git a/lib/search.ts b/lib/search.ts
new file mode 100644
index 00000000..878b7bed
--- /dev/null
+++ b/lib/search.ts
@@ -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(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(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(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)
+}
+
diff --git a/package.json b/package.json
index c6f18779..1cca657d 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,6 @@
"@tw-classed/react": "^1.8.0",
"@types/node": "^20.19.0",
"@types/prismjs": "^1.26.5",
- "algoliasearch": "^4",
"autoprefixer": "^10.4.14",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
@@ -60,7 +59,6 @@
"react": "^18.2.0",
"react-cookie": "^7.0.1",
"react-dom": "^18.2.0",
- "react-instantsearch-hooks-web": "^6.47.3",
"react-markdown": "^8.0.7",
"react-slick": "^0.30.3",
"react-use": "^17.4.0",
diff --git a/tests/api/api-test-suite.test.ts b/tests/api/api-test-suite.test.ts
index 891c83cb..ca6e3f84 100644
--- a/tests/api/api-test-suite.test.ts
+++ b/tests/api/api-test-suite.test.ts
@@ -36,7 +36,7 @@ describe("API Test Coverage", () => {
it("covers all critical API functionality", () => {
const expectedTestCategories = [
"Content APIs (articles, projects)",
- "Search functionality (Algolia integration)",
+ "Search functionality (local)",
"External integrations (Discord, YouTube)",
"RSS feed generation",
"Error handling and validation",
diff --git a/tests/api/search.test.ts b/tests/api/search.test.ts
index 66530dc7..cbded817 100644
--- a/tests/api/search.test.ts
+++ b/tests/api/search.test.ts
@@ -1,33 +1,51 @@
import { GET } from "@/app/api/search/route"
import { NextRequest } from "next/server"
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
-
-// Mock algoliasearch
-const mockSearch = vi.fn()
-const mockInitIndex = vi.fn()
-
-vi.mock("algoliasearch", () => ({
- default: vi.fn((appId: string, apiKey: string) =>
- appId && apiKey
- ? {
- initIndex: mockInitIndex,
- }
- : null
- ),
+import { describe, it, expect, vi, beforeEach } from "vitest"
+
+// Mock content functions
+const mockGetArticles = vi.fn()
+const mockGetProjects = vi.fn()
+
+vi.mock("@/lib/content", () => ({
+ getArticles: () => mockGetArticles(),
+ getProjects: () => mockGetProjects(),
}))
-// Mock environment variables
-const originalEnv = process.env
+// Mock search functions (uses real implementation)
+vi.mock("@/lib/search", async () => {
+ const actual = await vi.importActual("@/lib/search")
+ return actual
+})
describe("/api/search", () => {
beforeEach(() => {
vi.clearAllMocks()
- process.env = { ...originalEnv }
- mockInitIndex.mockReturnValue({ search: mockSearch })
- })
-
- afterEach(() => {
- process.env = originalEnv
+ mockGetArticles.mockReturnValue([
+ {
+ id: "test-article",
+ title: "Test Article",
+ content: "This is test content",
+ 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 = {}) => {
@@ -38,19 +56,6 @@ describe("/api/search", () => {
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", () => {
it("returns empty results when query is empty", async () => {
const request = createMockRequest({ query: "" })
@@ -61,9 +66,8 @@ describe("/api/search", () => {
expect(data).toEqual({
results: [],
status: "empty",
- availableIndexes: [],
+ availableIndexes: ["blog", "projects", "research"],
})
- expect(mockSearch).not.toHaveBeenCalled()
})
it("returns empty results when query is whitespace only", async () => {
@@ -75,64 +79,304 @@ describe("/api/search", () => {
expect(data).toEqual({
results: [],
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 = ""
+ it("searches articles by title", async () => {
+ const request = createMockRequest({ query: "test article" })
+ const response = await GET(request)
+ const data = await response.json()
+
+ expect(response.status).toBe(200)
+ expect(data.status).toBe("success")
+ expect(data.results.length).toBeGreaterThan(0)
+ const blogResult = data.results.find((r: any) => r.indexName === "blog")
+ expect(blogResult).toBeDefined()
+ expect(blogResult.hits[0].title).toBe("Test Article")
+ })
- const request = createMockRequest({ query: "test" })
+ 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(500)
- expect(data).toEqual({
- error: "Search client not initialized - missing Algolia credentials",
- availableIndexes: [],
- })
+ 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("returns error for search when credentials are invalid", async () => {
- // Test with invalid but present credentials
- process.env.ALGOLIA_APP_ID = ""
- process.env.ALGOLIA_SEARCH_API_KEY = ""
+ it("searches research index separately", async () => {
+ const request = createMockRequest({ query: "research", index: "research" })
+ const response = await GET(request)
+ const data = await response.json()
- const request = createMockRequest({
- query: "test query",
- index: "blog",
- })
+ 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(500)
- expect(data).toEqual({
- error: "Search client not initialized - missing Algolia credentials",
- availableIndexes: [],
- })
+ 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("handles search errors gracefully for specific index", async () => {
- const error = new Error("Algolia search failed")
+ 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)
+ })
- // Set up valid credentials but mock will reject
- process.env.ALGOLIA_APP_ID = ""
- process.env.ALGOLIA_SEARCH_API_KEY = ""
+ 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: "test",
- index: "blog",
- })
+ const request = createMockRequest({ query: "article", hitsPerPage: "2" })
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: [],
+ 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)
+ })
+ })
+
+ // New tests for tag matching
+ 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",
+ },
+ ])
+
+ 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("privacy-article")
+ })
+
+ it("matches tags across multiple tag fields in projects", async () => {
+ mockGetProjects.mockReturnValue([
+ {
+ 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)
})
})
})
diff --git a/yarn.lock b/yarn.lock
index 0e5609ba..5b7789e1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12,181 +12,6 @@ __metadata:
languageName: node
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":
version: 5.2.0
resolution: "@alloc/quick-lru@npm:5.2.0"
@@ -385,7 +210,7 @@ __metadata:
languageName: node
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
resolution: "@babel/runtime@npm:7.28.2"
checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a
@@ -2425,13 +2250,6 @@ __metadata:
languageName: node
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":
version: 1.0.5
resolution: "@types/estree-jsx@npm:1.0.5"
@@ -2448,13 +2266,6 @@ __metadata:
languageName: node
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":
version: 2.3.10
resolution: "@types/hast@npm:2.3.10"
@@ -2473,13 +2284,6 @@ __metadata:
languageName: node
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":
version: 3.3.7
resolution: "@types/hoist-non-react-statics@npm:3.3.7"
@@ -2592,13 +2396,6 @@ __metadata:
languageName: node
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":
version: 18.3.7
resolution: "@types/react-dom@npm:18.3.7"
@@ -3171,13 +2968,6 @@ __metadata:
languageName: node
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":
version: 3.0.1
resolution: "abbrev@npm:3.0.1"
@@ -3240,40 +3030,6 @@ __metadata:
languageName: node
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":
version: 7.0.0
resolution: "ansi-escapes@npm:7.0.0"
@@ -5972,18 +5728,6 @@ __metadata:
languageName: node
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":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@@ -5993,13 +5737,6 @@ __metadata:
languageName: node
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":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
@@ -6199,29 +5936,6 @@ __metadata:
languageName: node
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":
version: 1.1.0
resolution: "internal-slot@npm:1.1.0"
@@ -8346,13 +8060,6 @@ __metadata:
languageName: node
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":
version: 3.0.1
resolution: "mkdirp@npm:3.0.1"
@@ -8487,7 +8194,6 @@ __metadata:
"@vitejs/plugin-react": "npm:^4.7.0"
"@vitest/coverage-v8": "npm:3.2.4"
"@vitest/ui": "npm:^3.2.4"
- algoliasearch: "npm:^4"
autoprefixer: "npm:^10.4.14"
class-variance-authority: "npm:^0.4.0"
clsx: "npm:^1.2.1"
@@ -8520,7 +8226,6 @@ __metadata:
react: "npm:^18.2.0"
react-cookie: "npm:^7.0.1"
react-dom: "npm:^18.2.0"
- react-instantsearch-hooks-web: "npm:^6.47.3"
react-markdown: "npm:^8.0.7"
react-slick: "npm:^0.30.3"
react-use: "npm:^17.4.0"
@@ -8649,17 +8354,6 @@ __metadata:
languageName: node
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":
version: 8.1.0
resolution: "nopt@npm:8.1.0"
@@ -9123,13 +8817,6 @@ __metadata:
languageName: node
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":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -9227,13 +8914,6 @@ __metadata:
languageName: node
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":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -9266,36 +8946,6 @@ __metadata:
languageName: node
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":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -10014,13 +9664,6 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "section-matter@npm:1.0.0"
@@ -11602,7 +11245,7 @@ __metadata:
languageName: node
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
resolution: "use-sync-external-store@npm:1.5.0"
peerDependencies: