UNPKG

astro-loader-hashnode

Version:

Astro content loader for seamlessly integrating Hashnode blog posts into your Astro website using the Content Layer API

245 lines (244 loc) 9.42 kB
/** * Posts Loader - Handles Hashnode blog posts */ import { BaseHashnodeLoader, paginateResults, flattenPaginatedResults, } from './base.js'; import { postSchema } from '../types/schema.js'; /** * Transform Hashnode post to Astro content format */ function transformHashnodePost(post) { return { // Core content id: post.id, cuid: post.cuid, title: post.title, subtitle: post.subtitle || '', brief: post.brief || '', slug: post.slug, url: post.url, content: { html: post.content?.html || '', markdown: post.content?.markdown || undefined, }, // Metadata publishedAt: post.publishedAt ? new Date(post.publishedAt) : new Date(), updatedAt: post.updatedAt ? new Date(post.updatedAt) : undefined, // Reading metadata - match schema field names readingTime: post.readTimeInMinutes || 0, wordCount: undefined, // Engagement metrics - match schema field names views: post.views || 0, reactions: post.reactionCount || 0, comments: post.responseCount || 0, replies: post.replyCount || 0, // Status flags isDraft: false, hasLatex: post.hasLatexInPost || false, // Hashnode-specific required fields hashnodeId: post.id, hashnodeUrl: post.url, // Author information - match schema structure author: { id: post.author.id, name: post.author.name, username: post.author.username, profilePicture: post.author.profilePicture || undefined, bio: post.author.bio?.text || post.author.bio?.html || undefined, url: post.author.socialMediaLinks?.website || undefined, social: { website: post.author.socialMediaLinks?.website || undefined, github: post.author.socialMediaLinks?.github || undefined, twitter: post.author.socialMediaLinks?.twitter || undefined, linkedin: post.author.socialMediaLinks?.linkedin || undefined, }, followersCount: post.author.followersCount || undefined, }, // Co-authors coAuthors: post.coAuthors?.map(author => ({ id: author.id, name: author.name, username: author.username, profilePicture: author.profilePicture || undefined, bio: author.bio?.html || undefined, url: undefined, social: undefined, followersCount: undefined, })) || undefined, // Visual content - match schema structure coverImage: post.coverImage ? { url: post.coverImage.url, alt: undefined, attribution: post.coverImage.attribution || undefined, isPortrait: post.coverImage.isPortrait || undefined, isAttributionHidden: post.coverImage.isAttributionHidden || undefined, } : undefined, // Taxonomies - match schema structure tags: (post.tags || []).map(tag => ({ id: tag.id || undefined, name: tag.name, slug: tag.slug, logo: undefined, tagline: undefined, followersCount: undefined, })), // Series information series: post.series ? { id: post.series.id, name: post.series.name || undefined, slug: post.series.slug || undefined, } : undefined, // SEO data - required field seo: { title: post.seo?.title || post.title, description: post.seo?.description || post.brief || '', }, // Open Graph data ogMetaData: post.ogMetaData?.image ? { image: post.ogMetaData.image, } : undefined, // Table of contents - match schema structure tableOfContents: post.features?.tableOfContents?.isEnabled && post.features.tableOfContents.items ? { isEnabled: true, items: post.features.tableOfContents.items.map(item => ({ id: item.id, level: item.level, parentId: item.parentId || undefined, slug: item.slug, title: item.title, })), } : undefined, // Comments data - match schema structure commentsData: post.comments ? { totalCount: post.comments.totalDocuments || 0, comments: post.comments.edges?.map(edge => ({ id: edge.node.id, dateAdded: edge.node.dateAdded, totalReactions: edge.node.totalReactions || 0, content: { html: edge.node.content?.html || '', markdown: edge.node.content?.markdown || undefined, }, author: { id: edge.node.author.id, name: edge.node.author.name, username: edge.node.author.username, profilePicture: edge.node.author.profilePicture || undefined, bio: undefined, url: undefined, social: undefined, followersCount: undefined, }, replies: edge.node.replies?.edges?.map(reply => reply.node) || undefined, })) || [], } : undefined, // Preferences preferences: { disableComments: post.preferences?.disableComments || undefined, stickCoverToBottom: post.preferences?.stickCoverToBottom || undefined, pinnedToBlog: post.preferences?.pinnedToBlog || undefined, isDelisted: post.preferences?.isDelisted || undefined, }, // Publication info - not included in transformation for now publication: undefined, }; } /** * Posts Loader Class */ export class PostsLoader extends BaseHashnodeLoader { options; constructor(options) { super({ ...options, collection: 'posts', schema: postSchema, }); this.options = options; } /** * Fetch posts data from Hashnode API */ async fetchData() { const { maxPosts, includeDrafts = false, includeComments = false, includeCoAuthors = false, includeTableOfContents = false, filterByTags, } = this.options; // Handle drafts (requires authentication) if (includeDrafts) { if (!this.config.token) { throw new Error('Authentication token required for accessing drafts'); } const drafts = await this.client.getDrafts({ first: maxPosts }); return drafts.me.drafts.edges.map(edge => edge.node); } // Handle tag filtering if (filterByTags && filterByTags.length > 0) { const allPosts = []; for (const tagSlug of filterByTags) { const paginatedPosts = paginateResults(async (cursor) => { const result = await this.client.getPostsByTag(tagSlug, { first: 20, after: cursor, }); return { items: result.publication.posts.edges.map(edge => edge.node), pageInfo: result.publication.posts.pageInfo, }; }, maxPosts); const posts = await flattenPaginatedResults(paginatedPosts); allPosts.push(...posts); } // Remove duplicates and sort by published date const uniquePosts = Array.from(new Map(allPosts.map(post => [post.id, post])).values()).sort((a, b) => new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime()); return maxPosts ? uniquePosts.slice(0, maxPosts) : uniquePosts; } // Default: fetch all posts with pagination const paginatedPosts = paginateResults(async (cursor) => { const result = await this.client.getPosts({ first: 20, after: cursor, includeComments, includeCoAuthors, includeTableOfContents, }); return { items: result.publication.posts.edges.map(edge => edge.node), pageInfo: result.publication.posts.pageInfo, }; }, maxPosts); return flattenPaginatedResults(paginatedPosts); } /** * Transform Hashnode post to Astro content format */ transformItem(post) { return transformHashnodePost(post); } /** * Generate ID for post (prefer slug over cuid over id) */ generateId(post) { return post.slug || post.cuid || post.id; } } /** * Create a posts loader */ export function createPostsLoader(options) { return new PostsLoader(options); } /** * Create an Astro Loader for posts */ export function postsLoader(options) { return createPostsLoader(options).createLoader(); }