astro-loader-hashnode
Version:
Astro content loader for seamlessly integrating Hashnode blog posts into your Astro website using the Content Layer API
151 lines (150 loc) • 5.03 kB
JavaScript
/**
* Search Loader - Handles Hashnode search functionality
*/
import { BaseHashnodeLoader, paginateResults, flattenPaginatedResults, } from './base.js';
import { searchResultSchema } from '../types/schema.js';
/**
* Transform search result to Astro content format
*/
function transformSearchResult(post, searchTerm) {
return {
// Core content
id: post.id,
title: post.title,
brief: post.brief || '',
slug: post.slug,
url: post.url,
// Search metadata
searchTerm,
searchRelevance: calculateRelevance(post, searchTerm),
// Publication date
publishedAt: post.publishedAt ? new Date(post.publishedAt) : new Date(),
// Stats
reactionCount: post.reactionCount || 0,
views: post.views || 0,
// Author information
author: {
id: post.author?.id || '',
name: post.author?.name || '',
username: post.author?.username || '',
profilePicture: post.author?.profilePicture || '',
},
// Cover image
coverImage: post.coverImage
? {
url: post.coverImage.url,
}
: undefined,
// Publication info
publication: post.publication
? {
title: post.publication.title,
url: post.publication.url,
}
: undefined,
// Raw data for advanced use cases
raw: {
cuid: post.cuid,
},
};
}
/**
* Calculate search relevance score (simple implementation)
*/
function calculateRelevance(post, searchTerm) {
const term = searchTerm.toLowerCase();
let score = 0;
// Title match gets highest score
if (post.title.toLowerCase().includes(term)) {
score += 10;
}
// Brief/description match
if (post.brief?.toLowerCase().includes(term)) {
score += 5;
}
// Reaction count and views contribute to relevance
score += Math.min((post.reactionCount || 0) / 10, 3);
score += Math.min((post.views || 0) / 1000, 2);
return Math.round(score * 10) / 10; // Round to 1 decimal place
}
/**
* Search Loader Class
*/
export class SearchLoader extends BaseHashnodeLoader {
options;
constructor(options) {
super({
...options,
collection: 'search',
schema: searchResultSchema,
});
this.options = options;
}
/**
* Fetch search results from Hashnode API
*/
async fetchData() {
const { searchTerms, maxResults = 50 } = this.options;
if (!searchTerms || searchTerms.length === 0) {
return [];
}
const allResults = [];
// Search for each term
for (const searchTerm of searchTerms) {
try {
const paginatedResults = paginateResults(async (cursor) => {
const result = await this.client.searchPosts(searchTerm, {
first: 20,
after: cursor,
});
return {
items: result.searchPostsOfPublication.edges.map(edge => ({
post: edge.node,
searchTerm,
})),
pageInfo: result.searchPostsOfPublication.pageInfo,
};
}, Math.min(maxResults, 100) // Limit per search term
);
const results = await flattenPaginatedResults(paginatedResults);
allResults.push(...results);
}
catch {
// Silently continue with other search terms to avoid breaking the entire search
// In a production environment, you might want to use a proper logging service
// instead of console.warn
}
}
// Remove duplicates and sort by relevance
const uniqueResults = Array.from(new Map(allResults.map(result => [result.post.id, result])).values()).sort((a, b) => {
const relevanceA = calculateRelevance(a.post, a.searchTerm);
const relevanceB = calculateRelevance(b.post, b.searchTerm);
return relevanceB - relevanceA; // Descending order
});
return maxResults ? uniqueResults.slice(0, maxResults) : uniqueResults;
}
/**
* Transform search result to Astro content format
*/
transformItem(result) {
return transformSearchResult(result.post, result.searchTerm);
}
/**
* Generate ID for search result
*/
generateId(result) {
return `${result.searchTerm}-${result.post.slug || result.post.cuid || result.post.id}`;
}
}
/**
* Create a search loader
*/
export function createSearchLoader(options) {
return new SearchLoader(options);
}
/**
* Create an Astro Loader for search
*/
export function searchLoader(options) {
return createSearchLoader(options).createLoader();
}