-
-
Save onlime/1202b6a7c510aa60542b5c73b6d4e0a6 to your computer and use it in GitHub Desktop.
Algolia Vue InstantSearch component in Nuxt/content
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ALGOLIA_INDEX=dev_articles | |
ALGOLIA_APP_ID=ABCDE12345 | |
#ALGOLIA_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | |
ALGOLIA_SEARCH_ONLY_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | |
ALGOLIA_HITS_PER_PAGE=5 | |
ALGOLIA_QUERY_BUFFER_TIME=300 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/ --> | |
<ais-instant-search | |
:search-client="searchClient" | |
:index-name="$config.algoliaIndex" | |
:search-function="searchFunction" | |
> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/configure/vue/ --> | |
<!-- eslint-disable-next-line vue/attribute-hyphenation --> | |
<ais-configure :attributesToSnippet="['bodyPlainText']" :hits-per-page.camel="$config.algoliaHitsPerPage" /> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/autocomplete/vue/ --> | |
<ais-autocomplete v-click-outside="onClickOutside"> | |
<div slot-scope="{ currentRefinement, indices, refine }" class="relative"> | |
<div class="px-3 py-2 rounded-full flex items-center bg-gray-950 dark:bg-gray-700 dark:bg-opacity-70"> | |
<SvgSearchIcon /> | |
<input | |
ref="searchInput" | |
autocomplete="off" | |
type="search" | |
:value="currentRefinement" | |
:placeholder="searchPlaceholder" | |
class=" | |
ml-2 | |
w-full | |
sm:w-32 | |
md:w-40 md:focus:w-52 | |
bg-transparent | |
outline-none | |
transition-all | |
ease-in-out | |
duration-300 | |
placeholder-gray-400 | |
" | |
@input="refine($event.currentTarget.value)" | |
@focus="showResults = true" | |
@keydown.esc.prevent="onClickOutside" | |
@keydown.up.prevent="highlightPrevious(indices[0].hits.length)" | |
@keydown.down.prevent="highlightNext(indices[0].hits.length)" | |
@keydown.enter="goToArticle(indices)" | |
/> | |
</div> | |
<div | |
v-if="currentRefinement.length && showResults" | |
class="absolute sm:right-4 w-auto sm:w-104 z-10 transform mt-2 px-2 max-w-md sm:px-0" | |
> | |
<div class="rounded-md shadow-lg overflow-hidden"> | |
<!-- Lo-fi Tailwind CSS Tooltip - https://codepen.io/robstinson/pen/eYZLRdv --> | |
<div | |
class=" | |
absolute | |
-top-3 | |
left-10 | |
sm:left-auto sm:right-36 | |
w-6 | |
h-6 | |
rotate-45 | |
rounded-sm | |
bg-indigo-900 | |
" | |
/> | |
<div class="relative grid gap-6 text-gray-100 px-5 py-6 sm:gap-8 sm:p-5 bg-indigo-900"> | |
<div v-for="section in indices" :key="section.objectID" class="divide-y divide-indigo-600"> | |
<NuxtLink | |
v-for="(hit, index) in section.hits" | |
:key="hit.objectID" | |
:to="{ name: 'articles-slug', params: { slug: hit.objectID } }" | |
class="block text-sm col-span-2 py-2 transition ease-in-out duration-150" | |
:class="{ | |
'bg-indigo-700 transition duration-200 ease-in-out transform hover:scale-105': | |
isCurrentIndex(index), | |
}" | |
@click.native="onSearchResultClick" | |
> | |
<div class="px-2 text-left" @mouseover="highlightedIndex = index"> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/highlight/vue/ --> | |
<ais-highlight | |
attribute="title" | |
:hit="hit" | |
class="block text-green-400 font-semibold text-base" | |
/> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/snippet/vue/ --> | |
<ais-snippet attribute="bodyPlainText" :hit="hit" class="block text-gray-200" /> | |
</div> | |
</NuxtLink> | |
</div> | |
<!-- https://www.algolia.com/doc/api-reference/widgets/powered-by/vue/ --> | |
<ais-powered-by theme="dark" class="px-2" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</ais-autocomplete> | |
</ais-instant-search> | |
</template> | |
<script> | |
import algoliasearch from 'algoliasearch/lite' | |
import vClickOutside from 'v-click-outside' | |
export default { | |
directives: { | |
clickOutside: vClickOutside.directive, | |
}, | |
emits: ['results-closed'], | |
data() { | |
return { | |
showResults: false, | |
highlightedIndex: -1, | |
currentQuery: null, | |
} | |
}, | |
computed: { | |
searchPlaceholder() { | |
// navigator.platform may be 'iPhone', 'MacIntel', 'Mac???', 'Win32', 'Windows' | |
if (navigator.platform.includes('Mac')) { | |
return 'Search - ⌘k to focus' | |
} else if (navigator.platform.includes('Win')) { | |
return 'Search - ⊞k to focus' | |
} else { | |
return 'Search' | |
} | |
}, | |
searchClient() { | |
// By default, InstantSearch sends an initial request to Algolia’s servers with an empty query. | |
// This connection helps speed up later requests. | |
// But we want to limit the number of search requests and reduce your overall Algolia usage. | |
// https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/vue/ | |
// https://github.com/algolia/doc-code-samples/tree/master/Vue%20InstantSearch/conditional-request | |
const algoliaClient = algoliasearch(this.$config.algoliaAppId, this.$config.algoliaSearchOnlyKey) | |
return { | |
...algoliaClient, | |
search(requests) { | |
if (requests.every(({ params }) => !params.query)) { | |
return Promise.resolve({ | |
results: requests.map(() => ({ | |
hits: [], | |
nbHits: 0, | |
processingTimeMS: 0, | |
})), | |
}) | |
} | |
return algoliaClient.search(requests) | |
}, | |
} | |
}, | |
}, | |
watch: { | |
$route() { | |
this.showResults = false | |
this.$refs.searchInput.blur() | |
}, | |
}, | |
mounted() { | |
this.$nextTick(function () { | |
window.addEventListener('keydown', (event) => { | |
if ((event.metaKey || event.ctrlKey) && event.key === 'k') { | |
this.$refs.searchInput.select() | |
event.preventDefault() | |
} | |
}) | |
}) | |
}, | |
methods: { | |
// https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-function | |
searchFunction(helper) { | |
this.currentQuery = helper.state.query | |
if (helper.state.query) { | |
// buffer search queries, so that an Algolia search is not triggered if another | |
// search query has overwritten the currentQuery during the same 300ms (ALGOLIA_QUERY_BUFFER_TIME env var) | |
setTimeout(() => { | |
if (this.currentQuery === helper.state.query) { | |
// console.log('Algolia search called with query: ' + helper.state.query) | |
// console.log(helper) | |
helper.search() | |
// ensure that search results are always shown, even if they were hidden by previous navigation to | |
// same route that was already active | |
this.showResults = true | |
} | |
}, this.$config.algoliaQueryBufferTime) | |
} else { | |
// on empty search query, fire search immediately (will send local response, see computed searchClient()) | |
// console.log('Algolia search called with query: ' + helper.state.query) | |
helper.search() | |
} | |
}, | |
onClickOutside() { | |
this.showResults = false | |
// make sure focus is removed from search field, e.g. on esc key | |
this.$refs.searchInput.blur() | |
}, | |
highlightPrevious(resultsCount) { | |
if (this.highlightedIndex > 0 && this.highlightedIndex <= resultsCount) { | |
this.highlightedIndex -= 1 | |
} else { | |
this.highlightedIndex = resultsCount - 1 | |
} | |
}, | |
highlightNext(resultsCount) { | |
if (this.highlightedIndex < resultsCount - 1) { | |
this.highlightedIndex += 1 | |
} else { | |
this.highlightedIndex = 0 | |
} | |
}, | |
isCurrentIndex(index) { | |
return index === this.highlightedIndex | |
}, | |
onSearchResultClick() { | |
// ensure search results are closed, even if navigating to current route | |
this.onClickOutside() | |
// ensure mobile menu is closed, once we navigate to search result via click | |
this.$emit('results-closed') | |
}, | |
goToArticle(indices) { | |
if (indices[0].hits[this.highlightedIndex] === undefined) { | |
// search results are not yet loaded, maybe enter key pressed to early | |
return | |
} | |
this.onClickOutside() | |
// ensure mobile menu is closed, once we navigate to search result via keyboard | |
this.$emit('results-closed') | |
// this.$nuxt.$router.push(indices[0].hits[this.highlightedIndex].objectID) | |
this.$nuxt.$router.push({ | |
name: 'articles-slug', | |
params: { slug: indices[0].hits[this.highlightedIndex].objectID }, | |
}) | |
}, | |
}, | |
} | |
</script> | |
<style> | |
.ais-Highlight-highlighted, | |
.ais-Snippet-highlighted { | |
@apply bg-pink-500 text-white p-0.5 rounded-sm; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default { | |
// ... | |
publicRuntimeConfig: { | |
algoliaAppId: process.env.ALGOLIA_APP_ID, | |
algoliaSearchOnlyKey: process.env.ALGOLIA_SEARCH_ONLY_KEY, | |
algoliaIndex: process.env.ALGOLIA_INDEX || 'articles', | |
algoliaHitsPerPage: process.env.ALGOLIA_HITS_PER_PAGE || 5, | |
algoliaQueryBufferTime: process.env.ALGOLIA_QUERY_BUFFER_TIME || 300, | |
}, | |
plugins: [ | |
'~/plugins/vue-instantsearch', | |
], | |
build: { | |
transpile: [ | |
'vue-instantsearch', | |
'instantsearch.js/es', | |
], | |
}, | |
buildModules: [ | |
'nuxt-content-algolia', | |
], | |
nuxtContentAlgolia: { | |
appId: process.env.ALGOLIA_APP_ID, | |
// !IMPORTANT secret key should always be an environment variable | |
// this is not your search only key but the key that grants access to modify the index | |
apiKey: process.env.ALGOLIA_API_KEY, | |
// relative to content directory - each path get's its own index | |
paths: [ | |
{ | |
name: 'articles', | |
index: process.env.ALGOLIA_INDEX || 'articles', | |
fields: ['title', 'description', 'bodyPlainText', 'tags'], | |
}, | |
], | |
}, | |
hooks: { | |
'content:file:beforeInsert': (document) => { | |
if (document.extension === '.md') { | |
const removeMd = require('remove-markdown') | |
document.bodyPlainText = removeMd(document.text) | |
} | |
}, | |
}, | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Vue from 'vue' | |
import InstantSearch from 'vue-instantsearch' | |
Vue.use(InstantSearch) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment