Content Management API を使って自分の記事を全件取得してみる。
はじめに
皆様こんにちは、あかいけです。
最近、Contentfulで管理している記事をローカル環境にダウンロードする機会があり、いろいろ調べる機会がありました。
Contentfulには複数のAPIがあるのですが、今回は「Content Management API」を使って自分が作成した記事を全て取得してみました。
ContentfulのAPIの種類
Contentfulで利用できるAPIの一覧は以下ドキュメントの通りです。
この中で、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を使用することにしました。
実装の流れ
実装は大きく分けて以下の流れで進めます。
- Personal Access Token (PAT)の取得
- 必要な情報の収集(Space ID、User IDなど)
- Management APIクライアントの初期化
- 自分が作成した記事の取得
- 取得した記事をマークダウンファイルとして保存
では、順番に見ていきましょう。
準備
0. ライブラリのインストール
まずは必要なライブラリをインストールします、
今回は公式のNode.jsライブラリを利用します。
npm install contentful-management
1. Personal Access Token (PAT)の取得
次に、ContentfulのダッシュボードからPersonal Access Tokenを取得します。
以下のような流れで取得できます。
-
- Contentfulにログイン
-
- 右上のユーザーアイコンから「Account settings」を選択
-
- 「CMA tokens」タブを選択
-
- 「Create personal access token」をクリック
-
- トークン名と有効期限を入力して「Generate」をクリック
-
- 表示されたトークンをコピー
2. Space IDの確認
Space IDはContentfulのダッシュボードから確認することもできますが、
せっかくなのでManagement APIを使って取得してみます。
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を使って取得してみます。
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
記事を取得してマークダウンファイルとして保存する
では、実際に記事を取得してマークダウンファイルとして保存するコードを実装していきます。
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();
このコードでは、以下の処理を行っています。
- 出力先ディレクトリを作成(
./downloaded-posts) - エントリを取得(作成日時の降順でソート)
- 各エントリに対して以下を実行:
- フロントマター(メタデータ)を生成
- 本文を抽出
- マークダウンファイルとして保存
- 成功件数を集計して表示
実行結果
実際に実行してみると、以下のような出力が得られます。
$ node download-contentful-posts.js
取得: 3件
1. サンプル記事タイトル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']のようにアクセスする必要があります。
ロケールが不明な場合は、最初のロケールを動的に取得する実装にしました。
2. 公開・下書きの判定
エントリが公開済みか下書きかは、sys.publishedVersionの有無で判定できます。
この値が存在すれば公開済み(または公開済みで変更中)、存在しなければ下書きです。
sys.archivedVersionが存在 → アーカイブ済みsys.publishedVersionが存在し、sys.version == sys.publishedVersion + 1→ 公開済み(変更なし)sys.publishedVersionが存在し、sys.version >= sys.publishedVersion + 2→ 公開済み(未公開の変更あり)- それ以外 → 下書き
今回のコードでは全件取得するため、シンプルにsys.publishedVersionの有無だけで判定しています。
さいごに
以上、Content Management APIで自分が作成した記事を全て取得してみた話でした。
今回の実装により、以下のことができるようになりました。
- 自分が作成した全ての記事(公開済み・下書き含む)を取得
- 記事をマークダウンファイルとしてローカルに保存
- メタデータをフロントマターとして保存
ただし、Management APIはPersonal Access Tokenを使用するため、セキュリティには十分注意が必要です。
そのため以下の点を考慮して、安全に利用しましょう。
- トークンは絶対にGitリポジトリにコミットしない
- 環境変数や
.envファイル(.gitignoreに追加)で管理する - トークンに最小限の有効期限を設定する
- 不要になったトークンはContentfulの管理画面から削除する
この記事が、Contentfulを使っている方のお役に立てば幸いです。






