doesitarm/helpers/pagefind/index.js
ThatGuySam e1da6eb880 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.
2026-03-15 13:42:07 -05:00

181 lines
5 KiB
JavaScript

import fs from 'fs-extra'
import * as pagefind from 'pagefind'
import {
isNonEmptyArray,
isNonEmptyString
} from '~/helpers/check-types.js'
import {
getRouteType
} from '~/helpers/app-derived.js'
import {
getAppCategory
} from '~/helpers/categories.js'
import {
pagefindLanguage,
pagefindOutputPath
} from '~/helpers/pagefind/config.js'
function getSearchListing ( sitemapEntry ) {
return sitemapEntry.payload.app || sitemapEntry.payload.listing || sitemapEntry.payload.video || null
}
function pushContentPart ( parts, value ) {
if ( !isNonEmptyString( value ) ) return
parts.push( value.trim() )
}
function pushListContentPart ( parts, values ) {
if ( !isNonEmptyArray( values ) ) return
pushContentPart( parts, values.join(', ') )
}
function normalizeFilterValue ( value ) {
if ( !isNonEmptyString( value ) ) return null
return value.replaceAll('-', '_').trim()
}
export function shouldIndexSitemapEntry ( sitemapEntry ) {
return getSearchListing( sitemapEntry ) !== null
}
export function makePagefindTitle ( sitemapEntry ) {
const listing = getSearchListing( sitemapEntry )
const routeType = getRouteType( sitemapEntry.route )
let title = listing?.name || sitemapEntry.route
if ( routeType === 'benchmarks' ) {
title = `${ title } Benchmarks`
}
return title
}
export function makePagefindContent ( sitemapEntry ) {
const listing = getSearchListing( sitemapEntry )
const routeType = getRouteType( sitemapEntry.route )
const parts = []
pushContentPart( parts, makePagefindTitle( sitemapEntry ) )
pushContentPart( parts, listing?.text )
pushContentPart( parts, listing?.content )
pushContentPart( parts, listing?.description )
pushListContentPart( parts, listing?.aliases )
pushListContentPart( parts, listing?.tags )
pushListContentPart( parts, listing?.timestamps?.map( timestamp => timestamp.fullText ) )
pushListContentPart( parts, listing?.appLinks?.map( appLink => appLink.name ) )
pushContentPart( parts, listing?.category?.label )
pushContentPart( parts, listing?.status )
if ( routeType === 'benchmarks' ) {
pushContentPart( parts, 'Benchmarks')
pushContentPart( parts, 'Apple Silicon App Tested')
}
return parts.join('\n\n')
}
export function makePagefindFilters ( sitemapEntry ) {
const listing = getSearchListing( sitemapEntry )
const routeType = getRouteType( sitemapEntry.route )
const filters = {
type: [ normalizeFilterValue( routeType ) ]
}
const status = normalizeFilterValue( listing?.status )
if ( status !== null ) {
filters.status = [ status ]
}
if ( listing?.category?.slug ) {
filters.category = [ getAppCategory( listing ).snakeSlug ]
}
return filters
}
export function mapSitemapEntryToPagefindRecord ( sitemapEntry ) {
if ( !shouldIndexSitemapEntry( sitemapEntry ) ) return null
const listing = getSearchListing( sitemapEntry )
const routeType = getRouteType( sitemapEntry.route )
const lastUpdatedTimestamp = String( listing?.lastUpdated?.timestamp || 0 )
return {
url: sitemapEntry.route,
content: makePagefindContent( sitemapEntry ),
language: pagefindLanguage,
meta: {
title: makePagefindTitle( sitemapEntry ),
text: listing?.text || '',
slug: listing?.slug || sitemapEntry.route,
categorySlug: listing?.category?.slug || 'uncategorized',
routeType,
lastUpdatedTimestamp
},
filters: makePagefindFilters( sitemapEntry ),
sort: {
updated: lastUpdatedTimestamp
}
}
}
export async function writePagefindIndex ( sitemapEndpoints, {
outputPath = pagefindOutputPath
} = {} ) {
await fs.remove( outputPath )
const {
errors,
index
} = await pagefind.createIndex({
forceLanguage: pagefindLanguage
})
if ( errors.length > 0 ) {
throw new Error(`Pagefind createIndex errors: ${ errors.join(', ') }`)
}
if ( !index ) {
throw new Error('Pagefind index was not created')
}
let recordCount = 0
try {
for ( const sitemapEntry of sitemapEndpoints ) {
const record = mapSitemapEntryToPagefindRecord( sitemapEntry )
if ( record === null ) continue
const response = await index.addCustomRecord( record )
if ( response.errors.length > 0 ) {
throw new Error(`Pagefind addCustomRecord errors for ${ sitemapEntry.route }: ${ response.errors.join(', ') }`)
}
recordCount += 1
}
const writeResponse = await index.writeFiles({
outputPath
})
if ( writeResponse.errors.length > 0 ) {
throw new Error(`Pagefind writeFiles errors: ${ writeResponse.errors.join(', ') }`)
}
return {
outputPath,
recordCount
}
} finally {
await index.deleteIndex().catch( () => null )
await pagefind.close().catch( () => null )
}
}