A powerful content loader for integrating Hashnode blog posts into your Astro website using the Content Layer API.
- π Built for Astro v5.0+ - Uses the new Content Layer API
- π‘ GraphQL Integration - Leverages Hashnode's powerful GraphQL API
- π Smart Caching - Incremental updates with change detection
- π Full TypeScript Support - Complete type safety with Zod validation
- π·οΈ Rich Metadata - Author info, tags, SEO data, reading time, and more
- π¨ Flexible Content - Access HTML content (Markdown available for drafts)
- π‘οΈ Error Resilient - Graceful fallbacks and comprehensive error handling
- β‘ Performance Optimized - Cursor-based pagination and selective field querying
- π Multiple Loaders - Posts, Series, Drafts, and Search loaders available
- π Authentication Support - Access private content and drafts with API tokens
pnpm add astro-loader-hashnode
# or
npm install astro-loader-hashnode
# or
yarn add astro-loader-hashnode
- Configure your content collection in
src/content.config.ts
:
import { defineCollection } from 'astro:content';
import { hashnodeLoader } from 'astro-loader-hashnode';
const blog = defineCollection({
loader: hashnodeLoader({
publicationHost: 'yourblog.hashnode.dev', // Your Hashnode publication URL
token: process.env.HASHNODE_TOKEN, // Optional: for private content
maxPosts: 100, // Optional: limit number of posts
}),
});
export const collections = {
blog,
};
- Use the content in your Astro pages:
---
// src/pages/blog/[...slug].astro
import { getCollection, getEntry } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: { post }
}));
}
interface Props {
post: CollectionEntry<'blog'>;
}
const { post } = Astro.props;
const { data } = post;
---
<html>
<head>
<title>{data.title}</title>
<meta name="description" content={data.brief} />
</head>
<body>
<article>
<h1>{data.title}</h1>
<p>By {data.author.name} β’ {data.readingTime} min read</p>
<div set:html={data.content.html} />
</article>
</body>
</html>
Option | Type | Default | Description |
---|---|---|---|
publicationHost |
string |
Required | Your Hashnode publication host (e.g., yourblog.hashnode.dev ) |
token |
string |
undefined |
Optional API token for accessing private content |
maxPosts |
number |
1000 |
Maximum number of posts to fetch |
includeDrafts |
boolean |
false |
Whether to include draft posts (requires token) |
Access different types of content with specialized loaders:
import { defineCollection } from 'astro:content';
import { postsLoader, seriesLoader, draftsLoader, searchLoader } from 'astro-loader-hashnode';
const blog = defineCollection({
loader: postsLoader({
publicationHost: 'yourblog.hashnode.dev',
maxPosts: 100,
includeComments: true,
includeCoAuthors: true,
}),
});
const series = defineCollection({
loader: seriesLoader({
publicationHost: 'yourblog.hashnode.dev',
includePosts: true,
}),
});
// Requires authentication token
const drafts = defineCollection({
loader: draftsLoader({
publicationHost: 'yourblog.hashnode.dev',
token: process.env.HASHNODE_TOKEN,
}),
});
const searchResults = defineCollection({
loader: searchLoader({
publicationHost: 'yourblog.hashnode.dev',
searchTerms: ['javascript', 'react', 'astro'],
}),
});
export const collections = {
blog,
series,
drafts,
searchResults,
};
Create a .env
file in your project root:
HASHNODE_TOKEN=your_hashnode_token_here
HASHNODE_PUBLICATION_HOST=yourblog.hashnode.dev
Each post includes comprehensive metadata:
{
// Core content
title: string;
brief: string;
content: {
html: string;
markdown?: string; // Available for drafts
};
// Publishing metadata
publishedAt: Date;
updatedAt?: Date;
// Media
coverImage?: {
url: string;
alt?: string;
};
// Taxonomies
tags: Array<{
name: string;
slug: string;
}>;
// Author
author: {
name: string;
username: string;
profilePicture?: string;
url?: string;
};
// SEO
seo: {
title?: string;
description?: string;
};
// Reading metadata
readingTime: number;
wordCount: number;
// Hashnode-specific
hashnodeId: string;
hashnodeUrl: string;
}
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'My blog powered by Hashnode',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.publishedAt,
description: post.data.brief,
link: `/blog/${post.id}/`,
})),
});
}
- Go to Hashnode Developer Settings
- Generate a new Personal Access Token
- Add it to your
.env
file asHASHNODE_TOKEN
Note: The API token is only required for accessing private content and drafts. Public posts work without authentication.
- Incremental Updates: Content digests prevent re-processing unchanged posts
- Cursor-based Pagination: Efficiently handles large publications
- Error Handling: Graceful error handling for API limits and network issues
- Smart Caching: Implements fallbacks for network failures
Try the demo project to see the loader in action:
cd examples/demo
pnpm install
pnpm run dev
Contributions are welcome! Please see our Contributing Guide for detailed information on:
- Development setup and workflow
- Testing guidelines
- Commit conventions
- Release process
- Code style requirements
For quick contributions: fork the repo, make your changes, and submit a pull request!
MIT License - see LICENSE file for details.
- Astro Documentation - Learn about Astro
- Astro Content Layer - Content Layer API guide
- Hashnode - The blogging platform
- Hashnode API Documentation - API reference
- Astro Discord - Get help from the community