Hashnode

Developer-focused blogging platform. Community and SEO features specialized for technical blogs.

CMSBlogDeveloper-focusedGraphQLAPIHeadless
License
Commercial
Language
Web Platform
Pricing
基本機能無料
Official Site
Visit Official Site

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

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`;