Skip to content

Commit e1da6eb

Browse files
committed
feat(search): add pagefind provider support
Add Pagefind indexing and browser search adapters behind a provider switch. This lets prebuild generate either Stork or Pagefind search artifacts and lets the existing search UI run against Pagefind while preserving scoped filters, excerpts, and result metadata.
1 parent 727f84e commit e1da6eb

12 files changed

Lines changed: 690 additions & 65 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ dist
9292
/static/**/*.json
9393
/static/**/*.toml
9494
/static/**/*.st
95+
/static/pagefind/
9596
/commits-data.json
9697
/static/tailwind.css
9798

build-lists.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import { makeSearchableList } from '~/helpers/searchable-list.js'
4141
import {
4242
writeStorkToml
4343
} from '~/helpers/stork/toml.js'
44+
import {
45+
writePagefindIndex
46+
} from '~/helpers/pagefind/index.js'
47+
import {
48+
getSearchProvider
49+
} from '~/helpers/search/config.js'
4450
import {
4551
KindListMemoized as KindList
4652
} from '~/helpers/api/kind.js'
@@ -735,9 +741,15 @@ class BuildLists {
735741
console.log('Building XML Sitemap')
736742
await saveSitemap( sitemapEndpoints.map( ({ route }) => route ) )
737743

738-
// Save stork toml index
739-
console.log('Building Stork toml index')
740-
await writeStorkToml( sitemapEndpoints )
744+
const searchProvider = getSearchProvider( process.env.PUBLIC_SEARCH_PROVIDER )
745+
746+
if ( searchProvider === 'stork' ) {
747+
console.log('Building Stork toml index')
748+
await writeStorkToml( sitemapEndpoints )
749+
} else {
750+
console.log('Building Pagefind index')
751+
await writePagefindIndex( sitemapEndpoints )
752+
}
741753

742754
console.log('Total Nuxt Endpoints', this.endpointMaps.nuxt.size )
743755
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )

components/search-stork.vue

Lines changed: 109 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
hasActiveFilter( button.query ) ? 'border-opacity-50 bg-darkest' : 'border-opacity-0 neumorphic-shadow-inner'
4040
]"
4141
:aria-label="`Filter list by ${button.label}`"
42-
@click="toggleFilter(button.query); queryResults(query)"
42+
@click="toggleFilter(button.query); queryResults()"
4343
>{{ button.label }}</button>
4444
</div>
4545
</div>
@@ -151,21 +151,15 @@
151151
</div>
152152

153153
<div
154-
v-if="listing.storkResult"
154+
v-if="listing.resultExcerptsMarkup?.length"
155155
class="text-xs leading-5 font-light"
156156
>
157157
<div
158-
v-for="(excerpt, excerptIndex) in listing.storkResult.excerpts"
158+
v-for="(excerptMarkup, excerptIndex) in listing.resultExcerptsMarkup"
159159
:key="`excerpt-${ excerptIndex }`"
160160
class="result-excerpt space-y-3"
161-
>
162-
<div
163-
v-for="(range, rangeIndex) in makeHighlightedMarkup( excerpt )"
164-
:key="`range-${ rangeIndex }`"
165-
166-
v-html="range"
167-
/>
168-
</div>
161+
v-html="excerptMarkup"
162+
/>
169163
</div>
170164
<!-- listing.lastUpdated: {{ listing.lastUpdated }} -->
171165
<template v-if="listing.lastUpdated">
@@ -291,9 +285,18 @@ import {
291285
import {
292286
getIconForListing
293287
} from '~/helpers/app-derived.js'
288+
import {
289+
PagefindClient,
290+
mapPagefindDataToListing
291+
} from '~/helpers/pagefind/browser.js'
292+
import {
293+
getSearchProvider
294+
} from '~/helpers/search/config.js'
295+
import {
296+
SearchFilters
297+
} from '~/helpers/search/filters.js'
294298
import {
295299
StorkClient,
296-
StorkFilters,
297300
makeHighlightedMarkup,
298301
makeHighlightedResultTitle
299302
} from '~/helpers/stork/browser.js'
@@ -304,7 +307,7 @@ import RelativeTime from '~/components/relative-time.vue'
304307
import ListSummary from '~/components/list-summary.vue'
305308
import ListEndButtons from '~/components/list-end-buttons.vue'
306309
307-
let storkClient = null
310+
const searchProvider = getSearchProvider( import.meta.env.PUBLIC_SEARCH_PROVIDER )
308311
309312
export default {
310313
components: {
@@ -347,15 +350,27 @@ export default {
347350
hasStartedAnyQuery: false,
348351
listingsResults: [],
349352
waitingForQuery: false,
350-
isSSR: import.meta.env.SSR
353+
isSSR: import.meta.env.SSR,
354+
searchClient: null,
355+
searchFilters: null,
356+
lastQueryId: 0
351357
}
352358
},
353359
computed: {
354-
storkQuery () {
360+
activeSearchProvider () {
361+
return searchProvider
362+
},
363+
activeQuery () {
355364
return [
356365
this.userTextQuery.trim(),
357366
...this.filterQueryList
358-
].join(' ')
367+
].filter( Boolean ).join(' ')
368+
},
369+
pagefindFilters () {
370+
const filters = new SearchFilters()
371+
filters.setFromStringArray( this.filterQueryList )
372+
373+
return filters.asPagefindFilters
359374
},
360375
appList () {
361376
return this.kindPage.items
@@ -390,7 +405,7 @@ export default {
390405
return this.baseFilters.length > 0
391406
},
392407
hasSearchInputText () {
393-
return this.userTextQuery.length > 0
408+
return this.userTextQuery.trim().length > 0
394409
},
395410
hasAnyUserFilters () {
396411
return this.userFilters.length > 0
@@ -402,7 +417,7 @@ export default {
402417
return !this.hasAnyUserTerms
403418
},
404419
inputTerms () {
405-
return this.userTextQuery.trim().split(' ')
420+
return this.userTextQuery.trim().split(' ').filter( Boolean )
406421
},
407422
userFilters () {
408423
// console.log('filterQueryList', )
@@ -442,22 +457,23 @@ export default {
442457
}
443458
},
444459
mounted () {
445-
// Setup stork client
446-
storkClient = new StorkClient()
460+
this.searchClient = this.makeSearchClient()
447461
448-
// Store filter instance
449-
this.storkFilters = new StorkFilters()
450-
451-
// Add initial filters
452-
this.storkFilters.setFromStringArray( this.baseFilters )
462+
this.searchFilters = new SearchFilters()
453463
464+
this.searchFilters.setFromStringArray( this.baseFilters )
465+
this.filterQueryList = this.searchFilters.list
454466
},
455467
methods: {
456-
makeHighlightedMarkup,
457-
makeHighlightedResultTitle,
458-
459468
getIconForListing,
460469
470+
makeSearchClient () {
471+
if ( this.activeSearchProvider === 'stork' ) {
472+
return new StorkClient()
473+
}
474+
475+
return new PagefindClient()
476+
},
461477
getSearchLinks (app) {
462478
return app?.searchLinks || []
463479
},
@@ -466,26 +482,72 @@ export default {
466482
return this.filterQueryList.includes( filter )
467483
},
468484
toggleFilter ( newFilterQuery ) {
469-
470-
this.storkFilters.toggleFilter( newFilterQuery )
471-
472-
this.filterQueryList = this.storkFilters.list
485+
this.searchFilters.toggleFilter( newFilterQuery )
486+
this.filterQueryList = this.searchFilters.list
473487
},
474488
scrollInputToTop () {
475489
scrollIntoView(this.$refs['search-container'], {
476490
block: 'start',
477491
behavior: 'smooth'
478492
})
479493
},
494+
mapStorkResultToListing ( result ) {
495+
return {
496+
name: makeHighlightedResultTitle( result ),
497+
text: '',
498+
endpoint: result.entry.url,
499+
slug: result.entry.url,
500+
category: {
501+
slug: 'uncategorized'
502+
},
503+
lastUpdated: null,
504+
resultExcerptsMarkup: ( result.excerpts || [] ).flatMap( excerpt => {
505+
return makeHighlightedMarkup( excerpt )
506+
} )
507+
}
508+
},
509+
async runPagefindQuery () {
510+
const pagefindQuery = await this.searchClient.lazyQuery( this.userTextQuery, {
511+
filters: this.pagefindFilters,
512+
sort: this.hasSearchInputText ? {} : {
513+
updated: 'desc'
514+
}
515+
} )
516+
517+
if ( pagefindQuery === null ) {
518+
return null
519+
}
520+
521+
const resultData = await Promise.all( ( pagefindQuery.results || [] ).map( async result => {
522+
return await result.data()
523+
} ) )
524+
525+
return resultData.map( data => {
526+
return mapPagefindDataToListing( data, {
527+
highlightTerms: this.inputTerms
528+
} )
529+
} )
530+
},
531+
async runStorkQuery () {
532+
const storkQuery = await this.searchClient.lazyQuery( this.activeQuery, this.activeQuery.split(' ') )
480533
481-
// Called on input and when a filter is toggled
482-
async queryResults ( rawQuery ) {
534+
if ( storkQuery === null ) {
535+
return null
536+
}
537+
538+
return ( storkQuery.results || [] ).map( result => {
539+
return this.mapStorkResultToListing( result )
540+
} )
541+
},
483542
484-
console.log( 'query', this.storkQuery )
543+
// Called on input and when a filter is toggled
544+
async queryResults ( rawQuery = this.userTextQuery ) {
545+
const queryId = ++this.lastQueryId
485546
486-
// If our query is empty
487-
// then bail
488-
if ( this.storkQuery.trim().length === 0 ) return
547+
if ( this.activeQuery.trim().length === 0 ) {
548+
this.waitingForQuery = false
549+
return
550+
}
489551
490552
this.waitingForQuery = true
491553
@@ -494,36 +556,22 @@ export default {
494556
// Declare that at least one query has been made
495557
this.hasStartedAnyQuery = true
496558
497-
// console.log('rawQuery', rawQuery)
559+
const results = this.activeSearchProvider === 'stork'
560+
? await this.runStorkQuery()
561+
: await this.runPagefindQuery()
498562
499-
const requiredTerms = this.storkQuery.split(' ')
500-
501-
const storkQuery = await storkClient.lazyQuery( this.storkQuery, requiredTerms )
563+
if ( queryId !== this.lastQueryId ) {
564+
return
565+
}
502566
503-
// If the query response is empty
504-
// then return
505-
if ( storkQuery === null ) {
567+
if ( results === null ) {
568+
this.waitingForQuery = false
506569
return
507570
}
508571
509-
// console.log( 'storkQuery', storkQuery )
510-
511-
this.listingsResults = storkQuery.results.map( result => {
512-
return {
513-
name: makeHighlightedResultTitle( result ),
514-
endpoint: result.entry.url,
515-
slug: '',
516-
category: {
517-
slug: 'uncategorized'
518-
},
519-
storkResult: result
520-
}
521-
})
572+
this.listingsResults = results
522573
523-
// Switch from loading state and reveal the results
524574
this.waitingForQuery = false
525-
526-
// console.log('this.listingsResults', this.listingsResults)
527575
},
528576
529577
handleSearchInput ( event ) {

0 commit comments

Comments
 (0)