Content Management API を使って自分の記事を全件取得してみる。

Content Management API を使って自分の記事を全件取得してみる。

2025.11.03

はじめに

皆様こんにちは、あかいけです。

最近、Contentfulで管理している記事をローカル環境にダウンロードする機会があり、いろいろ調べる機会がありました。

Contentfulには複数のAPIがあるのですが、今回は「Content Management API」を使って自分が作成した記事を全て取得してみました。

ContentfulのAPIの種類

Contentfulで利用できるAPIの一覧は以下ドキュメントの通りです。

https://www.contentful.com/developers/docs/references/

この中で、Contentfulの記事取得に利用できるAPIとして以下の3種類があります。

Content Delivery API (CDA)

  • 用途: 公開済みコンテンツの取得(読み取り専用)
  • 認証: Content Delivery APIトークン
  • 取得できるコンテンツ: 公開済み(Published)のエントリのみ

Content Preview API (CPA)

  • 用途: 未公開コンテンツのプレビュー(読み取り専用)
  • 認証: Content Preview APIトークン
  • 取得できるコンテンツ: 下書きや公開前のコンテンツ

Content Management API (CMA)

  • 用途: コンテンツの作成・更新・削除、全てのコンテンツの取得
  • 認証: Personal Access Token (PAT)
  • 取得できるコンテンツ: 公開済み・下書き・アーカイブなど全ての状態

今回は下書きも含めて全ての記事を取得し、
さらにマークダウンファイルとして保存する必要があるため、Content Management APIを使用することにしました。

実装の流れ

実装は大きく分けて以下の流れで進めます。

  1. Personal Access Token (PAT)の取得
  2. 必要な情報の収集(Space ID、User IDなど)
  3. Management APIクライアントの初期化
  4. 自分が作成した記事の取得
  5. 取得した記事をマークダウンファイルとして保存

では、順番に見ていきましょう。

準備

0. ライブラリのインストール

まずは必要なライブラリをインストールします、
今回は公式のNode.jsライブラリを利用します。

https://github.com/contentful/contentful-management.js

npm install contentful-management

1. Personal Access Token (PAT)の取得

次に、ContentfulのダッシュボードからPersonal Access Tokenを取得します。
以下のような流れで取得できます。

    1. Contentfulにログイン
    1. 右上のユーザーアイコンから「Account settings」を選択
    1. 「CMA tokens」タブを選択
    1. 「Create personal access token」をクリック
    1. トークン名と有効期限を入力して「Generate」をクリック
    1. 表示されたトークンをコピー

2. Space IDの確認

Space IDはContentfulのダッシュボードから確認することもできますが、
せっかくなのでManagement APIを使って取得してみます。

get-spaces.js
const contentfulManagement = require('contentful-management');

const PAT = 'YOUR_PERSONAL_ACCESS_TOKEN';

const client = contentfulManagement.createClient({
  accessToken: PAT
});

async function getSpaces() {
  try {
    const spaces = await client.getSpaces();

    spaces.items.forEach((space) => {
      console.log('Space Name:', space.name);
      console.log('Space ID:', space.sys.id);
      console.log('');
    });
  } catch (error) {
    console.error('エラー:', error.message);
  }
}

getSpaces();

実行すると、以下のようにアクセス可能な全てのSpaceの名前とIDが表示されます。

Space Name: SpaceName
Space ID: XXXXXXXXXXXX

3. User IDの確認

User IDも同様にManagement APIを使って取得してみます。

get-user-id.js
const contentfulManagement = require('contentful-management');

const PAT = 'YOUR_PERSONAL_ACCESS_TOKEN';

const client = contentfulManagement.createClient({
  accessToken: PAT
});

async function getUserId() {
  try {
    const currentUser = await client.getCurrentUser();
    console.log('User ID:', currentUser.sys.id);
  } catch (error) {
    console.error('エラー:', error.message);
  }
}

getUserId();

実行すると、以下のように自身のユーザーIDが表示されます。

User ID: XXXXXXXXXXXXXXXXXXXXXX

記事を取得してマークダウンファイルとして保存する

では、実際に記事を取得してマークダウンファイルとして保存するコードを実装していきます。

download-contentful-posts.js
const contentfulManagement = require('contentful-management');
const fs = require('fs');
const path = require('path');

const SPACE_ID = 'YOUR_SPACE_ID';
const PAT = 'YOUR_PERSONAL_ACCESS_TOKEN';
const ENVIRONMENT_ID = 'master';
const USER_ID = 'YOUR_USER_ID';
const OUTPUT_DIR = './downloaded-posts';

const client = contentfulManagement.createClient({
  accessToken: PAT
});

// ディレクトリが存在しない場合は作成
function ensureDirectoryExists(dirPath) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }
}

// ファイル名として使用できる文字列に変換
function sanitizeFilename(filename) {
  return filename
    .replace(/[<>:"/\\|?*]/g, '-')  // 無効な文字を置換
    .replace(/\s+/g, '-')  // スペースをハイフンに
    .substring(0, 100);  // 長さ制限
}

// マークダウンファイルを生成
function generateMarkdown(entry) {
  const fields = entry.fields;
  const sys = entry.sys;
  const contentType = sys.contentType.sys.id;
  const status = sys.publishedVersion ? '公開済み' : '下書き';

  // フロントマター
  let frontmatter = '---\n';
  frontmatter += `id: ${sys.id}\n`;
  frontmatter += `contentType: ${contentType}\n`;
  frontmatter += `status: ${status}\n`;
  frontmatter += `createdAt: ${sys.createdAt}\n`;
  frontmatter += `updatedAt: ${sys.updatedAt}\n`;

  // フィールドをフロントマターに追加(contentフィールド以外)
  Object.keys(fields).forEach(key => {
    if (key !== 'content' && key !== 'body') {
      const fieldValue = fields[key]?.[Object.keys(fields[key])[0]];
      if (fieldValue && typeof fieldValue !== 'object') {
        frontmatter += `${key}: ${fieldValue}\n`;
      }
    }
  });

  frontmatter += '---\n\n';

  // 本文を取得(contentまたはbodyフィールドを探す)
  let content = '';
  const locale = Object.keys(fields)[0] ? Object.keys(fields[Object.keys(fields)[0]])[0] : 'ja-JP';

  if (fields.content && fields.content[locale]) {
    content = fields.content[locale];
  } else if (fields.body && fields.body[locale]) {
    content = fields.body[locale];
  } else {
    // その他のフィールドをすべて出力
    Object.keys(fields).forEach(key => {
      const fieldValue = fields[key]?.[locale];
      if (fieldValue) {
        content += `## ${key}\n\n`;
        if (typeof fieldValue === 'string') {
          content += `${fieldValue}\n\n`;
        } else {
          content += `${JSON.stringify(fieldValue, null, 2)}\n\n`;
        }
      }
    });
  }

  return frontmatter + content;
}

async function downloadAllPosts() {
  try {
    // 出力ディレクトリを作成
    ensureDirectoryExists(OUTPUT_DIR);

    // SpaceとEnvironmentを取得
    const space = await client.getSpace(SPACE_ID);
    const environment = await space.getEnvironment(ENVIRONMENT_ID);

    // エントリを取得
    const entries = await environment.getEntries({
      'sys.createdBy.sys.id': USER_ID,
      limit: 1000,
      order: '-sys.createdAt'
    });

    console.log(`取得: ${entries.items.length}`);

    let successCount = 0;

    // 各記事をマークダウンファイルとして保存
    entries.items.forEach((entry, index) => {
      try {
        const contentType = entry.sys.contentType.sys.id;
        const locale = entry.fields[Object.keys(entry.fields)[0]]
          ? Object.keys(entry.fields[Object.keys(entry.fields)[0]])[0]
          : 'ja-JP';

        // ファイル名を生成(タイトルまたはslugがあれば使用)
        let filename;
        if (entry.fields.title && entry.fields.title[locale]) {
          filename = sanitizeFilename(entry.fields.title[locale]);
        } else if (entry.fields.slug && entry.fields.slug[locale]) {
          filename = sanitizeFilename(entry.fields.slug[locale]);
        } else {
          filename = `${contentType}-${entry.sys.id}`;
        }

        filename = `${filename}.md`;

        // マークダウンを生成
        const markdown = generateMarkdown(entry);

        // ファイルに保存
        const filepath = path.join(OUTPUT_DIR, filename);
        fs.writeFileSync(filepath, markdown, 'utf8');

        successCount++;
        console.log(`${index + 1}. ${filename}`);

      } catch (error) {
        console.error(`エラー: ${entry.sys.id} - ${error.message}`);
      }
    });

    console.log(`\n完了: ${successCount}`);
    console.log(`保存先: ${path.resolve(OUTPUT_DIR)}`);

  } catch (error) {
    console.error('エラー:', error.message);
  }
}

// 実行
downloadAllPosts();

このコードでは、以下の処理を行っています。

  1. 出力先ディレクトリを作成(./downloaded-posts)
  2. エントリを取得(作成日時の降順でソート)
  3. 各エントリに対して以下を実行:
    • フロントマター(メタデータ)を生成
    • 本文を抽出
    • マークダウンファイルとして保存
  4. 成功件数を集計して表示

実行結果

実際に実行してみると、以下のような出力が得られます。

$ node download-contentful-posts.js
取得: 31. サンプル記事タイトル1.md
2. サンプル記事タイトル2.md
3. サンプル記事タイトル3.md

完了: 3件
保存先: /Users/username/downloaded-posts

保存されたマークダウンファイルは以下のような形式になります。

---
id: 1A2B3C4D5E
contentType: blogPost
status: 公開済み
createdAt: 2024-01-15T10:30:00.000Z
updatedAt: 2024-01-20T15:45:00.000Z
title: サンプル記事タイトル
slug: sample-article
---

## 本文内容

ここに記事の本文が入ります...

ハマったポイント

実装する際にいくつかハマったポイントがあったので、共有します。

1. ロケール(locale)の扱い

Contentfulのフィールドはロケールごとに値を持っているため、fields.titleではなくfields.title['ja-JP']のようにアクセスする必要があります。
ロケールが不明な場合は、最初のロケールを動的に取得する実装にしました。

https://www.contentful.com/developers/docs/concepts/locales/

2. 公開・下書きの判定

エントリが公開済みか下書きかは、sys.publishedVersionの有無で判定できます。
この値が存在すれば公開済み(または公開済みで変更中)、存在しなければ下書きです。

  • sys.archivedVersionが存在 → アーカイブ済み
  • sys.publishedVersionが存在し、sys.version == sys.publishedVersion + 1 → 公開済み(変更なし)
  • sys.publishedVersionが存在し、sys.version >= sys.publishedVersion + 2 → 公開済み(未公開の変更あり)
  • それ以外 → 下書き

今回のコードでは全件取得するため、シンプルにsys.publishedVersionの有無だけで判定しています。

https://www.contentful.com/developers/docs/tutorials/general/determine-entry-asset-state/

さいごに

以上、Content Management APIで自分が作成した記事を全て取得してみた話でした。
今回の実装により、以下のことができるようになりました。

  • 自分が作成した全ての記事(公開済み・下書き含む)を取得
  • 記事をマークダウンファイルとしてローカルに保存
  • メタデータをフロントマターとして保存

ただし、Management APIはPersonal Access Tokenを使用するため、セキュリティには十分注意が必要です。
そのため以下の点を考慮して、安全に利用しましょう。

  • トークンは絶対にGitリポジトリにコミットしない
  • 環境変数や.envファイル(.gitignoreに追加)で管理する
  • トークンに最小限の有効期限を設定する
  • 不要になったトークンはContentfulの管理画面から削除する

この記事が、Contentfulを使っている方のお役に立てば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事