👌
【Zenn API・Next.js】Zennの記事をAPIで取得してみよう
はじめに
WebサイトにZennの記事を取得して表示させる機会があったので、その方法をご紹介します。
公式からは、APIのドキュメントは発表されていませんが、先人の方たちの記事を参考にさせていただきました。
今回一番やりたかったことは、「Topics ごとに分類したい」でしたが、APIでは記事のTopicsが取得できません。
今回は書きませんが、GitHub経由であれば可能ですので、また記事にしたいと思います。
環境
- Next.js 15.4.3
npx create-next-app@latest zenn-get-data --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
Zenn API
ユーザー情報取得 API
エンドポイント
//https://zenn.dev/api/users/username=(ユーザー名)
https://zenn.dev/api/users/zenn
レスポンス
{
"user": {
"id": 2,
"username": "zenn",
"name": "Zenn公式",
"avatar_small_url": "https://res.cloudinary.com/zenn/image/fetch/s--MvSHFDIS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/9965dabc76.jpeg",
"avatar_url": "https://storage.googleapis.com/zenn-user-upload/avatar/9965dabc76.jpeg",
"bio": "Zennの使い方や開発状況を発信する公式アカウントです。",
"autolinked_bio": "Zennの使い方や開発状況を発信する公式アカウントです。",
"github_username": "zenn-dev",
"twitter_username": "zenn_dev",
"is_support_open": false,
"tokusyo_contact": null,
"tokusyo_name": null,
"website_url": null,
"website_domain": null,
"total_liked_count": 6123,
"ga_tracking_id": null,
"hatena_id": null,
"is_invoice_issuer": false,
"follower_count": 2206,
"following_count": 0,
"following_user_count": 0,
"following_publication_count": 0,
"badge_count": 5,
"articles_count": 20,
"books_count": 1,
"scraps_count": 0,
"awards": []
}
}
記事取得 API
エンドポイント
//https://zenn.dev/api/articles?username=(ユーザー名)
https://zenn.dev/api/articles?username=zenn
クエリパラメータ
- page : ページ番号
- count : 一度に取得する記事数
- order : 並び替え順
- order=latest
- topicname : トピック
- username : ユーザー名
- article_type :
techoridea
https://zenn.dev/api/articles?username=zenn&order=latest
※ Zenn APIは1ページあたり最大48件
全件取得する場合は、ページネーションを使用して、ループ処理をして取得しましょう。
後ほどコードを紹介します。
レスポンス
{
"articles": [
{
"id": 358729,
"post_type": "Article",
"title": "PublicationにGitHubリポジトリを連携してZennのコンテンツを管理する",
"slug": "connect-to-github-publication",
"comments_count": 0,
"liked_count": 4,
"bookmarked_count": 3,
"body_letters_count": 3960,
"article_type": "idea",
"emoji": "😼",
"is_suspending_private": false,
"published_at": "2025-01-15T14:28:38.434+09:00",
"body_updated_at": "2025-02-05T17:00:28.387+09:00",
"source_repo_updated_at": "2025-02-05T17:00:28.322+09:00",
"pinned": false,
"path": "/zenn/articles/connect-to-github-publication",
"principal_type": "User",
"user": {
"id": 2,
"username": "zenn",
"name": "Zenn公式",
"avatar_small_url": "https://res.cloudinary.com/zenn/image/fetch/s--MvSHFDIS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/9965dabc76.jpeg"
},
"publication": null
}
]
}
実装
シンプルな取得方法
型定義
//ZennAPI
export interface ZennArticle {
id: number;
title: string;
slug: string;
published_at: string;
emoji: string;
path: string;
user: {
username: string;
name: string;
}
}
import { ZennArticle } from '@/types';
export default async function Home() {
const res = await fetch ('https://zenn.dev/api/articles?username=zenn&order=latest');
const data = await res.json();
const articles: ZennArticle[] = data.articles;
console.log(articles);
return(
<main className="container mx-auto px-4 py-8">
<section>
<h2 className="text-2xl font-semibold mb-4">最新記事</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{articles.map((article) => (
<div key={article.id} className="border rounded-lg p-4 hover:shadow-lg">
<a
href={`https://zenn.dev${article.path}`}
target="_blank"
className="block"
>
<h3 className="font-medium">
{article.emoji} {article.title}
</h3>
<p className="text-sm text-gray-500 mt-2">
{new Date(article.published_at).toLocaleDateString('ja-JP')}
</p>
<p className="text-xs text-gray-400">
by {article.user.name}
</p>
</a>
</div>
))}
</div>
</section>
</main>
)
}

48件しか取得できていませんね。
全件取得方法
型定義は上記と同じです。
import { ZennArticle } from '@/types';
export default async function Home() {
const allArticles: ZennArticle[] = [];
let currentPage = 1;
let hasMorePages = true;
// 全ページを取得するループ
while (hasMorePages) {
const res = await fetch(
`https://zenn.dev/api/articles?username=zenn&order=latest&page=${currentPage}`
);
const data = await res.json();
if (data.articles?.length > 0) {
allArticles.push(...data.articles);
if (data.next_page === null) {
hasMorePages = false;
} else {
currentPage = data.next_page;
}
} else {
hasMorePages = false;
}
}
console.log(`全記事数: ${allArticles.length}件`);
return (
<main className="container mx-auto px-4 py-8">
<section>
<h2 className="text-2xl font-semibold mb-4">全記事 ({allArticles.length}件)</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{allArticles.map((article) => (
<div key={article.id} className="border rounded-lg p-4 hover:shadow-lg">
<a
href={`https://zenn.dev${article.path}`}
target="_blank"
className="block"
>
<h3 className="font-medium">
{article.emoji} {article.title}
</h3>
<p className="text-sm text-gray-500 mt-2">
{new Date(article.published_at).toLocaleDateString('ja-JP')}
</p>
<p className="text-xs text-gray-400">
by {article.user.name}
</p>
</a>
</div>
))}
</div>
</section>
</main>
);
}
少し解説です。
const res = await fetch(
`https://zenn.dev/api/articles?username=t_oishi&order=latest&page=${currentPage}`
);
const data = await res.json();
- 1回目:
page=1→ 48件取得 - 2回目:
page=2→ 48件取得 - 3回目:
page=3→ 10件取得
if (data.articles?.length > 0)
-
data.articlesが存在すればlengthをチェック - 存在しなければ
undefinedを返す(エラーにならない)
if (data.next_page === null) {
hasMorePages = false;
} else {
currentPage = data.next_page;
}
次ページの判定
Zenn APIレスポンス例
`{
"articles": [...],
"next_page": 2 *// 次のページがある場合*
}
{
"articles": [...],
"next_page": null *// 最後のページの場合*
}`
`} else {
hasMorePages = false;
}`
記事が0件の場合、ループを終了する
全体の流れ
1回目: page=1 → 48件取得 → next_page=2 → 続行
2回目: page=2 → 48件取得 → next_page=3 → 続行
3回目: page=3 → 10件取得 → next_page=null → 終了

これで全件取得できました。
Discussion