A powerful content loader for integrating Hashnode blog posts into your Astro website using the Astro Content Layer API.
- 🚀 Built for Astro v5.0+ – Uses the new Content Layer API
- 📡 GraphQL Integration – Leverages Hashnode's GraphQL API
- 🔄 Smart Caching – Incremental updates with change detection
- 🧵 Digest-based Incremental Loads – Skips unchanged entries (faster rebuilds)
- �️ Rendered HTML Support – Each entry includes
rendered.html
forrender(entry)
usage - 🧪 Schema Auto-Exposure – Loader exports its internal Zod schema (you can override)
- 📌 Extra Preferences – Includes
pinnedToBlog
andisDelisted
when available - �📝 Full TypeScript Support – Complete type safety with Zod validation
- 🏷️ Rich Metadata – Author info, tags, SEO/OG data, reading time, TOC, etc.
- 🎨 Flexible Content – HTML always; Markdown for drafts (and optionally for posts when provided by API)
- 🛡️ Error Resilient – Graceful fallbacks and structured loader errors
- ⚡ Performance Optimized – Cursor-based pagination & selective field querying
- 📚 Multiple Loaders – Posts, Series, Drafts, Search (more can be added)
- 🔐 Authentication Support – Access drafts & private data with a token
pnpm add astro-loader-hashnode
# or
npm install astro-loader-hashnode
# or
yarn add astro-loader-hashnode
import { defineCollection } from 'astro:content';
import { hashnodeLoader } from 'astro-loader-hashnode';
const blog = defineCollection({
loader: hashnodeLoader({
publicationHost: 'yourblog.hashnode.dev', // Required
token: process.env.HASHNODE_TOKEN, // Optional
maxPosts: 100, // Optional
}),
});
export const collections = { blog };
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({ params: { slug: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { data, render } = post; // render() available if you want Astro to render markdown
const html = data.content.html; // Pre-rendered HTML already available
---
<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={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;
// Preferences (optional when present)
preferences?: {
pinnedToBlog?: boolean;
isDelisted?: boolean;
disableComments?: boolean;
stickCoverToBottom?: boolean;
};
// 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
- Schema Reuse: Exposed schema aids IDE inference without extra config
- Rendered HTML: Avoids re-render cost when you just need HTML directly
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
- Issues
- Discussions
- For security issues, please do not open a public issue—email the maintainer instead.