Hashnode
開発者向けブログプラットフォーム。技術ブログに特化したコミュニティとSEO機能。
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の大幅な変更は困難
- プラットフォーム依存: データエクスポートは限定的
- 日本語コミュニティ小: 英語中心のコミュニティ
- 収益化機能なし: 有料購読機能は未提供
主要リンク
- Hashnode公式サイト
- Hashnode Documentation
- Hashnode API Documentation
- GraphQL Playground
- Hashnode Support
- Hashnode Discord
使い方の例
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`;