👌

【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 : tech or idea
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 → 終了

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

GitHubで編集を提案

Discussion