diff --git a/package-lock.json b/package-lock.json
index 2330bd69..eb073793 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "coryd.dev",
- "version": "1.3.0",
+ "version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "coryd.dev",
- "version": "1.3.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
"@cdransf/api-text": "^1.5.0",
@@ -21,7 +21,7 @@
"@11ty/eleventy": "v3.0.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
"@cdransf/eleventy-plugin-tabler-icons": "^2.0.3",
- "@supabase/supabase-js": "^2.45.5",
+ "@supabase/supabase-js": "^2.45.6",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"dotenv-flow": "^4.1.0",
@@ -614,9 +614,9 @@
}
},
"node_modules/@supabase/postgrest-js": {
- "version": "1.16.2",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.2.tgz",
- "integrity": "sha512-dA/CIrSO2YDQ6ABNpbvEg9DwBMMbuKfWaFuZAU9c66PenoLSoIoyXk1Yq/wC2XISgEIqaMHmTrDAAsO80kjHqg==",
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz",
+ "integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -647,16 +647,16 @@
}
},
"node_modules/@supabase/supabase-js": {
- "version": "2.45.5",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.5.tgz",
- "integrity": "sha512-xTPsv33Hcj6C38SXa4nKobwEwkNQuwcCKtcuBsDT6bvphl1VUAO3x2QoLOuuglJzk2Oaf3WcVsvRcxXNE8PG/g==",
+ "version": "2.45.6",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.6.tgz",
+ "integrity": "sha512-qVXSSUhhIqdFnF2VUGgeecPvw1cDW6+avcTbRgur4LaGnzrJCbM3Rx7g81/SSZjjeqYOtmHuKWhiHzV/EN8Ktw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.65.1",
"@supabase/functions-js": "2.4.3",
"@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "1.16.2",
+ "@supabase/postgrest-js": "1.16.3",
"@supabase/realtime-js": "2.10.7",
"@supabase/storage-js": "2.7.1"
}
@@ -709,9 +709,9 @@
"peer": true
},
"node_modules/@types/node": {
- "version": "22.7.6",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz",
- "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==",
+ "version": "22.7.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz",
+ "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
diff --git a/package.json b/package.json
index a511b29f..9b064dd0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "coryd.dev",
- "version": "1.3.0",
+ "version": "1.4.0",
"description": "The source for my personal site. Built using 11ty (and other tools).",
"type": "module",
"engines": {
@@ -39,7 +39,7 @@
"@11ty/eleventy": "v3.0.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
"@cdransf/eleventy-plugin-tabler-icons": "^2.0.3",
- "@supabase/supabase-js": "^2.45.5",
+ "@supabase/supabase-js": "^2.45.6",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"dotenv-flow": "^4.1.0",
diff --git a/src/assets/scripts/index.js b/src/assets/scripts/index.js
index b3011ae5..dc37c302 100644
--- a/src/assets/scripts/index.js
+++ b/src/assets/scripts/index.js
@@ -85,12 +85,11 @@ window.addEventListener('load', () => {
})
})()
+ // search logic
;(() => {
if (!MiniSearch) return
- const miniSearch = new MiniSearch({
- fields: ['title', 'text', 'tags', 'type'],
- })
+ const miniSearch = new MiniSearch({ fields: ['title', 'description', 'tags', 'type'] })
const $form = document.querySelector('.search__form')
const $input = document.querySelector('.search__form--input')
const $fallback = document.querySelector('.search__form--fallback')
@@ -105,47 +104,108 @@ window.addEventListener('load', () => {
const PAGE_SIZE = 10
let currentPage = 1
let currentResults = []
-
- const loadSearchIndex = async () => {
- try {
- const response = await fetch('https://coryd.dev/api/search')
- const index = await response.json()
- const resultsById = index.reduce((byId, result) => {
- byId[result.id] = result
- return byId
- }, {})
- miniSearch.addAll(index)
- return resultsById
- } catch (error) {
- console.error('Error fetching search index:', error)
- return {}
- }
- }
-
let resultsById = {}
let debounceTimeout
- loadSearchIndex().then(loadedResultsById => resultsById = loadedResultsById)
-
- const getSelectedTypes = () => {
- return Array.from($typeCheckboxes)
- .filter(checkbox => checkbox.checked)
- .map(checkbox => checkbox.value)
+ const parseMarkdown = markdown => {
+ if (!markdown) return ''
+ return markdown
+ .replace(/\*\*(.*?)\*\*/g, '$1')
+ .replace(/\*(.*?)\*/g, '$1')
+ .replace(/\[(.*?)\]\((.*?)\)/g, '$1')
+ .replace(/\n/g, '
')
+ .replace(/[#*_~`]/g, '')
}
- $input.addEventListener('input', () => {
- const query = $input.value
+ const truncateDescription = (markdown, maxLength = 150) => {
+ const htmlDescription = parseMarkdown(markdown)
+ const tempDiv = document.createElement('div')
+ tempDiv.innerHTML = htmlDescription
+ const plainText = tempDiv.textContent || tempDiv.innerText || ''
+ return plainText.length > maxLength ? `${plainText.substring(0, maxLength)}...` : plainText
+ }
+ const formatArtistTitle = (title, totalPlays) =>
+ totalPlays > 0 ? `${title} ${totalPlays} plays` : title
+
+ const renderSearchResults = results => {
+ if (results.length > 0) {
+ $results.innerHTML = results.map(({ title, url, description, type, total_plays }) => {
+ const truncatedDesc = truncateDescription(description)
+ const formattedTitle = type === 'artist' && total_plays !== undefined
+ ? formatArtistTitle(title, total_plays)
+ : title
+
+ return `
+
+
+ ${formattedTitle}
+
+ ${truncatedDesc}
+
+ `
+ }).join('')
+ $results.style.display = 'block'
+ } else {
+ $results.innerHTML = 'No results found.'
+ $results.style.display = 'block'
+ }
+ }
+
+ const appendSearchResults = results => {
+ const newResults = results.map(({ title, url, description, type, total_plays }) => {
+ const truncatedDesc = truncateDescription(description)
+ const formattedTitle = type === 'artist' && total_plays !== undefined
+ ? formatArtistTitle(title, total_plays)
+ : title
+
+ return `
+
+
+ ${formattedTitle}
+
+ ${truncatedDesc}
+
+ `
+ }).join('')
+ $results.insertAdjacentHTML('beforeend', newResults)
+ }
+
+ const loadSearchIndex = async (query = '', types = []) => {
+ const typeQuery = types.join(',')
+ const response = await fetch(`https://coryd.dev/api/search-beta?q=${query}&type=${typeQuery}`)
+ const index = await response.json()
+
+ resultsById = index.reduce((byId, result) => {
+ byId[result.id] = result
+ return byId
+ }, {})
+
+ miniSearch.removeAll()
+ miniSearch.addAll(index)
+ return resultsById
+ }
+
+ loadSearchIndex().then(loadedResultsById => resultsById = loadedResultsById)
+
+ const getSelectedTypes = () =>
+ Array.from($typeCheckboxes)
+ .filter(checkbox => checkbox.checked)
+ .map(checkbox => checkbox.value)
+
+ $input.addEventListener('input', () => {
+ const query = $input.value.trim()
clearTimeout(debounceTimeout)
- if (query.length === 0) {
+ if (!query) {
renderSearchResults([])
$loadMoreButton.style.display = 'none'
return
}
- debounceTimeout = setTimeout(() => {
- const results = (query.length > 1) ? getSearchResults(query) : []
+ debounceTimeout = setTimeout(async () => {
+ resultsById = await loadSearchIndex(query, getSelectedTypes())
+ const results = getSearchResults(query)
currentResults = results
currentPage = 1
@@ -154,14 +214,10 @@ window.addEventListener('load', () => {
}, 300)
})
- $input.addEventListener('keydown', (event) => {
- if (event.key === 'Enter') event.preventDefault()
- })
-
$typeCheckboxes.forEach(checkbox => {
- checkbox.addEventListener('change', () => {
- const query = $input.value
- const results = getSearchResults(query)
+ checkbox.addEventListener('change', async () => {
+ resultsById = await loadSearchIndex($input.value.trim(), getSelectedTypes())
+ const results = getSearchResults($input.value.trim())
currentResults = results
currentPage = 1
@@ -178,85 +234,12 @@ window.addEventListener('load', () => {
if (currentPage * PAGE_SIZE >= currentResults.length) $loadMoreButton.style.display = 'none'
})
- const getSearchResults = (query) => {
- const selectedTypes = getSelectedTypes()
-
- return miniSearch.search(query, { prefix: true, fuzzy: 0.2, boost: { title: 2 } })
+ const getSearchResults = query =>
+ miniSearch.search(query, { prefix: true, fuzzy: 0.2, boost: { title: 2 } })
.map(({ id }) => resultsById[id])
- .filter(result => selectedTypes.includes(result.type))
- }
+ .filter(result => getSelectedTypes().includes(result.type))
- const getResultsForPage = (page) => {
- const start = (page - 1) * PAGE_SIZE
- const end = page * PAGE_SIZE
- return currentResults.slice(start, end)
- }
-
- const parseMarkdown = (markdown) => {
- if (!markdown) return ''
- markdown = markdown.replace(/\*\*(.*?)\*\*/g, '$1')
- markdown = markdown.replace(/\*(.*?)\*/g, '$1')
- markdown = markdown.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
- markdown = markdown.replace(/\n/g, '
')
- markdown = markdown.replace(/[#*_~`]/g, '')
- return markdown
- }
-
- const truncateDescription = (markdown, maxLength = 150) => {
- const htmlDescription = parseMarkdown(markdown)
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = htmlDescription
- const plainText = tempDiv.textContent || tempDiv.innerText || ''
- if (plainText.length > maxLength) return plainText.substring(0, maxLength) + '...'
- return plainText
- }
-
- const formatArtistTitle = (title, totalPlays) => {
- if (totalPlays > 0) return `${title} ${totalPlays} plays`
- return `${title}`
- }
-
- const renderSearchResults = (results) => {
- if (results.length > 0) {
- $results.innerHTML = results.map(({ title, url, description, type, total_plays }) => {
- const truncatedDesc = truncateDescription(description)
- let formattedTitle = title
-
- if (type === 'artist' && total_plays !== undefined) formattedTitle = formatArtistTitle(title, total_plays)
-
- return `
-
-
- ${formattedTitle}
-
- ${truncatedDesc}
-
- `
- }).join('\n')
- $results.style.display = 'block'
- } else {
- $results.innerHTML = 'No results found.'
- $results.style.display = 'block'
- }
- }
-
- const appendSearchResults = (results) => {
- const newResults = results.map(({ title, url, description, type, total_plays }) => {
- const truncatedDesc = truncateDescription(description)
- let formattedTitle = title
-
- if (type === 'artist' && total_plays !== undefined) formattedTitle = formatArtistTitle(title, total_plays)
-
- return `
-
-
- ${formattedTitle}
-
- ${truncatedDesc}
-
- `
- }).join('\n')
- $results.insertAdjacentHTML('beforeend', newResults)
- }
+ const getResultsForPage = page =>
+ currentResults.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
})()
})
\ No newline at end of file
diff --git a/src/data/search.js b/src/data/search.js
deleted file mode 100644
index 006b4c7e..00000000
--- a/src/data/search.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createClient } from '@supabase/supabase-js'
-
-const SUPABASE_URL = process.env.SUPABASE_URL
-const SUPABASE_KEY = process.env.SUPABASE_KEY
-const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
-
-export default async function fetchSearchIndex() {
- const { data, error } = await supabase
- .from('optimized_search_index')
- .select('search_index')
-
- if (error) {
- console.error('Error fetching search index data:', error)
- return []
- }
-
- const [{ search_index } = {}] = data
- return search_index || []
-}
\ No newline at end of file
diff --git a/src/pages/data/search.json.liquid b/src/pages/data/search.json.liquid
deleted file mode 100644
index ae4ac018..00000000
--- a/src/pages/data/search.json.liquid
+++ /dev/null
@@ -1,4 +0,0 @@
----
-permalink: "/api/search"
----
-{{ search | json }}
\ No newline at end of file
diff --git a/views/feeds/search.psql b/views/feeds/search.psql
index c8dfb568..f14d1738 100644
--- a/views/feeds/search.psql
+++ b/views/feeds/search.psql
@@ -62,9 +62,7 @@ WITH search_data AS (
SELECT
'artist' AS type,
- CONCAT(
- COALESCE(ar.emoji, ar.genre_emoji, '🎧'), ' ', ar.name
- ) AS title,
+ CONCAT(COALESCE(ar.emoji, ar.genre_emoji, '🎧'), ' ', ar.name) AS title,
CONCAT('https://coryd.dev', ar.url) AS url,
ar.description AS description,
ARRAY[ar.genre_name] AS tags,
@@ -110,17 +108,13 @@ search_data_with_id AS (
FROM search_data
)
SELECT
- json_agg(
- json_build_object(
- 'id', search_data_with_id.id,
- 'url', search_data_with_id.url,
- 'title', search_data_with_id.title,
- 'description', search_data_with_id.description,
- 'tags', search_data_with_id.tags,
- 'genre_name', search_data_with_id.genre_name,
- 'genre_url', search_data_with_id.genre_url,
- 'type', search_data_with_id.type,
- 'total_plays', search_data_with_id.total_plays
- )
- ) AS search_index
+ id,
+ url,
+ title,
+ description,
+ tags,
+ genre_name,
+ genre_url,
+ type,
+ total_plays
FROM search_data_with_id;
\ No newline at end of file
diff --git a/workers/search/index.js b/workers/search/index.js
new file mode 100644
index 00000000..b63baecd
--- /dev/null
+++ b/workers/search/index.js
@@ -0,0 +1,50 @@
+import { createClient } from '@supabase/supabase-js'
+
+export default {
+ async fetch(request, env) {
+ const allowedOrigin = 'https://coryd.dev'
+ const origin = request.headers.get('Origin') || ''
+ const referer = request.headers.get('Referer') || ''
+
+ if (!origin.startsWith(allowedOrigin) && !referer.startsWith(allowedOrigin)) return new Response('Forbidden', { status: 403 })
+
+ const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL
+ const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY
+ const supabase = createClient(supabaseUrl, supabaseKey)
+
+ const { searchParams } = new URL(request.url)
+ const query = searchParams.get('q') || ''
+ const types = searchParams.get('type')?.split(',') || []
+ const page = parseInt(searchParams.get('page')) || 1
+ const pageSize = parseInt(searchParams.get('page_size')) || 10
+ const offset = (page - 1) * pageSize
+
+ try {
+ let supabaseQuery = supabase
+ .from('optimized_search_index')
+ .select('*', { count: 'exact' })
+
+ if (types.length > 0) supabaseQuery = supabaseQuery.in('type', types)
+ if (query) supabaseQuery = supabaseQuery.or(`title.ilike.%${query}%,description.ilike.%${query}%`)
+
+ const { data, error, count } = await supabaseQuery.range(offset, offset + pageSize - 1)
+
+ if (error) {
+ console.error('Query error:', error)
+ return new Response('Error fetching data from Supabase', { status: 500 })
+ }
+
+ return new Response(
+ JSON.stringify({ results: data, total: count, page, pageSize }),
+ {
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ }
+ )
+ } catch (error) {
+ console.error('Unexpected error:', error)
+ return new Response('Internal Server Error', { status: 500 })
+ }
+ }
+}
\ No newline at end of file
diff --git a/workers/search/wrangler.template.toml b/workers/search/wrangler.template.toml
new file mode 100644
index 00000000..a59b41f2
--- /dev/null
+++ b/workers/search/wrangler.template.toml
@@ -0,0 +1,12 @@
+name = "search-worker"
+main = "./index.js"
+compatibility_date = "2023-01-01"
+
+account_id = "${CF_ACCOUNT_ID}"
+workers_dev = true
+
+[env.production]
+name = "search-worker-production"
+routes = [
+ { pattern = "coryd.dev/api/search*", zone_id = "${CF_ZONE_ID}" },
+]
\ No newline at end of file