Hashnode
Developer-focused blogging platform. Community and SEO features specialized for technical blogs.
CMS
Hashnode
Overview
Hashnode is a developer-focused blogging platform that provides community and SEO features specialized for technical blogs.
Details
Hashnode is a blogging platform designed for developers and technical writers. Founded in 2016, it aims to promote building developer communities and sharing technical knowledge.
Hashnode's greatest feature is its developer-first design and API-first approach. Through GraphQL API, you have complete access to blog data and can build custom frontends in headless mode. By default, it includes all the features developers need: a refined editor, code highlighting, Markdown support, and GitHub integration.
As of 2024, over 1 million developers participate with more than 100,000 blogs in operation. It offers rich features necessary for technical blogs including free custom domain support, SEO optimization, team features, and AI-powered search and writing assistance. Enterprise plans also provide SSO, custom SLAs, and 99.99% uptime guarantee.
Pros and Cons
Pros
- Developer features: Code highlighting, GitHub gadgets, technical tags
- Free and feature-rich: Custom domain, backup, analytics for free
- GraphQL API: Full headless mode support
- SEO optimization: Structured data, automatic sitemap generation
- Community: Active developer-focused community
- AI integration: AI search and AI writing assist (free)
- Team support: Multi-author collaborative blog management
Cons
- Developer-specific: Not suitable for non-technical content
- Customization limits: Major UI changes are difficult
- Platform dependency: Limited data export
- Small Japanese community: English-centric community
- No monetization: Paid subscription features not available
Key Links
- Hashnode Official Site
- Hashnode Documentation
- Hashnode API Documentation
- GraphQL Playground
- Hashnode Support
- Hashnode Discord
Usage Examples
Basic GraphQL API Usage
// GraphQL API endpoint
const HASHNODE_API_URL = 'https://gql.hashnode.com/';
// Basic fetch function
async function gqlQuery(query, variables = {}) {
const response = await fetch(HASHNODE_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables
})
});
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0].message);
}
return data.data;
}
Fetching User Posts
// Query to fetch user's latest posts
const GET_USER_POSTS = `
query GetUserPosts($username: String!, $page: Int!) {
user(username: $username) {
id
name
profilePicture
tagline
publication {
id
title
domain
posts(page: $page) {
id
slug
title
brief
coverImage
dateAdded
totalReactions
responseCount
tags {
id
name
slug
}
}
}
}
}
`;
// Usage example
async function fetchUserPosts(username) {
try {
const data = await gqlQuery(GET_USER_POSTS, {
username: username,
page: 0
});
console.log(`Posts by ${data.user.name}:`);
data.user.publication.posts.forEach(post => {
console.log(`- ${post.title} (${post.totalReactions} reactions)`);
});
return data.user.publication.posts;
} catch (error) {
console.error('Error fetching posts:', error);
}
}
Fetching Publication Posts with Pagination
// Cursor-based pagination
const GET_PUBLICATION_POSTS = `
query GetPublicationPosts($host: String!, $first: Int!, $after: String) {
publication(host: $host) {
id
title
about
favicon
posts(first: $first, after: $after) {
edges {
node {
id
slug
title
brief
readTimeInMinutes
publishedAt
views
url
coverImage {
url
photographer
isAttributionRequired
}
tags {
id
name
slug
}
author {
id
name
username
profilePicture
bio
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
`;
// Pagination handling
async function fetchAllPublicationPosts(host) {
let allPosts = [];
let hasNextPage = true;
let after = null;
while (hasNextPage) {
const data = await gqlQuery(GET_PUBLICATION_POSTS, {
host: host,
first: 10,
after: after
});
const { edges, pageInfo } = data.publication.posts;
allPosts = allPosts.concat(edges.map(edge => edge.node));
hasNextPage = pageInfo.hasNextPage;
after = pageInfo.endCursor;
console.log(`Fetched ${allPosts.length} posts...`);
}
return allPosts;
}
React/Next.js Integration
// components/HashnodeBlog.jsx
import { useEffect, useState } from 'react';
const HashnodeBlog = ({ host }) => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
const query = `
query GetPosts($host: String!) {
publication(host: $host) {
title
posts(first: 6) {
edges {
node {
id
slug
title
brief
publishedAt
coverImage {
url
}
tags {
name
slug
}
author {
name
profilePicture
}
}
}
}
}
}
`;
try {
const response = await fetch('https://gql.hashnode.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables: { host }
})
});
const data = await response.json();
setPosts(data.data.publication.posts.edges.map(edge => edge.node));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, [host]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="hashnode-blog">
<h2>Latest Posts</h2>
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
{post.coverImage && (
<img src={post.coverImage.url} alt={post.title} />
)}
<div className="post-content">
<h3>{post.title}</h3>
<p>{post.brief}</p>
<div className="post-meta">
<img
src={post.author.profilePicture}
alt={post.author.name}
className="author-avatar"
/>
<span>{post.author.name}</span>
<time>
{new Date(post.publishedAt).toLocaleDateString('en-US')}
</time>
</div>
<div className="tags">
{post.tags.map(tag => (
<span key={tag.slug} className="tag">
#{tag.name}
</span>
))}
</div>
</div>
</article>
))}
</div>
</div>
);
};
export default HashnodeBlog;
Headless Mode Implementation
// Remix framework implementation example
// app/routes/blog/index.jsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
const HASHNODE_HOST = "yourblog.hashnode.dev";
export const loader = async () => {
const query = `
query GetPosts {
publication(host: "${HASHNODE_HOST}") {
id
title
displayTitle
descriptionSEO
favicon
isTeam
series(first: 10) {
edges {
node {
id
name
slug
description
coverImage
posts(first: 5) {
totalDocuments
}
}
}
}
posts(first: 10) {
edges {
node {
id
slug
title
brief
url
publishedAt
coverImage {
url
}
}
}
}
}
}
`;
const response = await fetch("https://gql.hashnode.com/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
});
const result = await response.json();
return json(result.data.publication);
};
export default function BlogIndex() {
const publication = useLoaderData();
return (
<div className="blog-container">
<header>
<h1>{publication.title}</h1>
<p>{publication.descriptionSEO}</p>
</header>
{publication.series.edges.length > 0 && (
<section className="series-section">
<h2>Series</h2>
<div className="series-grid">
{publication.series.edges.map(({ node: series }) => (
<div key={series.id} className="series-card">
<h3>{series.name}</h3>
<p>{series.description}</p>
<span>{series.posts.totalDocuments} posts</span>
</div>
))}
</div>
</section>
)}
<section className="posts-section">
<h2>Latest Posts</h2>
<div className="posts-list">
{publication.posts.edges.map(({ node: post }) => (
<article key={post.id}>
<a href={`/blog/${post.slug}`}>
<h3>{post.title}</h3>
<p>{post.brief}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</a>
</article>
))}
</div>
</section>
</div>
);
}
Webhook and RSS Utilization
// RSS feed URL
const RSS_FEED_URL = `https://${HASHNODE_HOST}/rss.xml`;
// Webhook configuration (Hashnode Dashboard → Integrations)
// Event examples:
// - New post published
// - Post updated
// - Comment added
// Webhook receiving endpoint implementation
app.post('/webhooks/hashnode', async (req, res) => {
const { event, post } = req.body;
switch (event) {
case 'post.published':
// Handle new post publication
await notifySubscribers({
title: post.title,
url: post.url,
brief: post.brief
});
break;
case 'post.updated':
// Handle post update
await updateCache(post.id);
break;
case 'comment.created':
// Handle comment addition
await sendCommentNotification(post);
break;
}
res.status(200).json({ received: true });
});
// Automatic sitemap generation URL
const SITEMAP_URL = `https://${HASHNODE_HOST}/sitemap.xml`;