Hashnode

開発者向けブログプラットフォーム。技術ブログに特化したコミュニティとSEO機能。

CMSブログ開発者向けGraphQLAPIヘッドレス
ライセンス
Commercial
言語
Web Platform
料金
基本機能無料

CMS

Hashnode

概要

Hashnodeは、開発者向けブログプラットフォームで、技術ブログに特化したコミュニティとSEO機能を提供します。

詳細

Hashnode(ハッシュノード)は、開発者とテクニカルライターのために設計されたブログプラットフォームです。2016年に設立され、開発者コミュニティの構築と技術知識の共有を促進することを目的としています。

Hashnodeの最大の特徴は、開発者ファーストの設計とAPIファーストアプローチです。GraphQL APIを通じて、ブログのデータに完全にアクセスでき、ヘッドレスモードで独自のフロントエンドを構築できます。標準では、洗練されたエディタ、コードハイライト、Markdown対応、GitHubとの連携など、開発者に必要な機能が全て揃っています。

2024年現在、100万人以上の開発者が参加し、10万以上のブログが運営されています。独自ドメインの無料サポート、SEO最適化、チーム機能、AIを活用した検索と執筆支援など、技術ブログに必要な機能が充実しています。企業向けプランでは、SSO、カスタムSLA、99.99%の稼働時間保証も提供されています。

メリット・デメリット

メリット

  • 開発者向け機能: コードハイライト、GitHubガジェット、技術タグ
  • 無料で高機能: 独自ドメイン、バックアップ、分析機能が無料
  • GraphQL API: 完全なヘッドレスモード対応
  • SEO最適化: 構造化データ、サイトマップ自動生成
  • コミュニティ: 開発者向けの活発なコミュニティ
  • AI機能統合: AI検索とAI執筆アシスト(無料)
  • チーム対応: 複数著者での共同ブログ運営

デメリット

  • 開発者特化: 非技術系コンテンツには不向き
  • カスタマイズ制限: 標準UIの大幅な変更は困難
  • プラットフォーム依存: データエクスポートは限定的
  • 日本語コミュニティ小: 英語中心のコミュニティ
  • 収益化機能なし: 有料購読機能は未提供

主要リンク

使い方の例

GraphQL APIの基本的な使い方

// GraphQL APIエンドポイント
const HASHNODE_API_URL = 'https://gql.hashnode.com/';

// 基本的なfetch関数
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;
}

ユーザーの記事を取得

// ユーザーの最新記事を取得するクエリ
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
          }
        }
      }
    }
  }
`;

// 使用例
async function fetchUserPosts(username) {
  try {
    const data = await gqlQuery(GET_USER_POSTS, {
      username: username,
      page: 0
    });
    
    console.log(`${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);
  }
}

Publicationの記事をページネーション付きで取得

// カーソルベースのページネーション
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
        }
      }
    }
  }
`;

// ページネーション処理
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(`${allPosts.length}件の記事を取得...`);
  }
  
  return allPosts;
}

React/Next.jsでの統合

// 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>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return (
    <div className="hashnode-blog">
      <h2>最新記事</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('ja-JP')}
                </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;

ヘッドレスモードでの実装

// Remix frameworkでの実装例
// 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>シリーズ</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}記事</span>
              </div>
            ))}
          </div>
        </section>
      )}

      <section className="posts-section">
        <h2>最新記事</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とRSSの活用

// RSS フィードのURL
const RSS_FEED_URL = `https://${HASHNODE_HOST}/rss.xml`;

// Webhookの設定(Hashnode Dashboard → Integrations)
// イベント例:
// - 新しい記事が公開された時
// - 記事が更新された時
// - コメントが追加された時

// Webhook受信エンドポイントの実装
app.post('/webhooks/hashnode', async (req, res) => {
  const { event, post } = req.body;
  
  switch (event) {
    case 'post.published':
      // 新記事公開時の処理
      await notifySubscribers({
        title: post.title,
        url: post.url,
        brief: post.brief
      });
      break;
      
    case 'post.updated':
      // 記事更新時の処理
      await updateCache(post.id);
      break;
      
    case 'comment.created':
      // コメント追加時の処理
      await sendCommentNotification(post);
      break;
  }
  
  res.status(200).json({ received: true });
});

// サイトマップの自動生成URL
const SITEMAP_URL = `https://${HASHNODE_HOST}/sitemap.xml`;