A content loader for Astro that pulls blog posts from Hashnode via the Content Layer API.
A set of loaders for Astro v5.0+ that fetch posts, series, drafts, and search results from Hashnode's GraphQL API. Supports incremental builds, full TypeScript types, and rendered HTML out of the box.
- 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 for faster rebuilds
- Rendered HTML Support – Each entry includes
rendered.htmlforrender(entry)usage - Schema Auto-Exposure – Loader exports its internal Zod schema (you can override)
- Full TypeScript Support – Complete type safety with Zod validation
- Rich Metadata – Author info, tags, SEO/OG data, reading time, TOC, and more
- Multiple Loaders – Posts, Series, Drafts, Search (more can be added)
- Authentication Support – Access drafts and private data with a token
pnpm add astro-loader-hashnode
# or
npm install astro-loader-hashnode
# or
yarn add astro-loader-hashnode- Go to Hashnode Developer Settings
- Generate a new Personal Access Token
- Add it to your
.envfile asHASHNODE_TOKEN
Note: The API token is only required for accessing private content and drafts. Public posts work without authentication.
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.devEach 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}/`,
})),
});
}- 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 devSee CONTRIBUTING.md for details.
ISC License - see LICENSE file for details.