ローカル管理のコンテンツを Contentful に移行する
CMS には Git ベースのものと API ベースのものがあり、Git ベースのものは GitHub などと連携して、CMS でのコンテンツの更新を GitHub にプッシュします。GitHub と Netlify や Vercel などのホスティングサービスが連携していれば、GitHub へのプッシュがデプロイを引き起こしてもとのサイトが更新されます。つまりコンテンツが同じリポジトリにあり、ローカルでは同じプロジェクトディレクトリにあります。
対して API ベースのものは、API によって記事の取得や更新を行うことができるタイプのものです。もとのサイトではコンテンツの取得も API で行うため、リポジトリ(プロジェクトディレクトリ)にコンテンツが存在しないのがふつうです。CMS での更新は Git の差分として現れないため、ホスティングサービスと直接連携することでもとのサイトを更新します。
コンテンツのある場所が違うので、Git ベースで管理していたコンテンツを API ベースの CMS に移す際には、API を利用してコンテンツを CMS 側に作成する必要があります。
この記事ではローカルで保管しているコンテンツを API ベースの Headless CMS のひとつである Contentful に移す手順を紹介します。
Content Management API
Contentful には Content Management API があるので、これを使っていきます。
プロジェクトにパッケージをインストールして、
yarn add contentful-management
よきところに main.js
(ts-node
などを使うなら ts
でも OK)を作成します。
基本的な使い方はこんな感じです。
main.ts1import { createClient } from 'contentful-management';23const client = createClient({4accessToken: '<access-token>' // Contentful のダッシュボードから取得したアクセストークンを指定5});67const main async () => {8const space = await client.getSpace('<space-id>'); // Space を取得9const env = await space.getEnvironment('<environment-id>'); // Environment を取得10const entry = await env.createEntry('<content-type-id>', {11fields: {12title: { ja: 'たいとる' },13content: { ja: 'てすと' },14}15});16};1718main();
公式のドキュメントなどでは .then
をつなげて書いていることが多いですが、async
await
を使って書くこともできます。
fields
には Contentful であらかじめ作成しておいた Content Model に合わせたものを指定します。
このコードをコマンドで走らせれば、新たな記事が作成されていることが Contentful のダッシュボードから確認できます。
node main.jsts-node main.ts
記事の移行
ローカルの .md
(または .mdx
)ファイルの内容を Contentful に移行します。
仮に記事のディレクトリ構成が以下のようになっているとします。
▼ 📂 posts▼ 📂 xxx🖼️ index.jpg🖼️ image.jpg📄 index.md▶ 📁 yyy▶ 📁 zzz
Markdown のフロントマターの情報を取得するために、gray-matter
を使います。
yarn add gray-matter
posts
下のすべての記事を移行する場合、移行コードは次のようになります。
main.ts1import fs from 'fs';2import { join } from 'path';3import matter from 'gray-matter';4import { createClient } from 'contentful-management';56const postsDirectory = join(process.cwd(), 'posts'); // 記事ディレクトリのパス78const getPostBySlug = (slug: string) => {9const postPath = join(postDirectory, `${slug}/index.md`);10const fileContents = fs.readFileSync(postPath, 'utf8');11const { data, content } = matter(fileContents);1213// data にフロントマターの情報が入る14// slug も必要なら加えておく15return { slug, content, ...data };16};1718const getPosts = () => {19const slugs = fs.readdirSync(postsDirectory);20const posts = slugs.map((slug: string) => getPostBySlug(slug));21return posts;22};2324const client = createClient({25accessToken: '<access-token>'26});2728const main async () => {29const space = await client.getSpace('<space-id>');30const env = await space.getEnvironment('<environment-id>');3132const posts = getPosts();33for (const post of posts) {34try {35const entry = await env.createEntry('<content-type-id>', {36fields: Object.fromEntries(37Object.entries(post).map(([k, v]) => [k, { ja: v }])38)39});40console.log(`\u001b[32m✅ Updated successfully: ${entry.fields.slug.ja}\u001b[0m`);41} catch (e) {42console.log(`\u001b[31m❌ Update failed: ${post.slug}\u001b[0m`);43console.log(e);44}45}46};4748main();
メディアファイルの移行
画像や動画など、Markdown 以外のコンテンツを移行します。
Contentful ではメディアファイルは Media というところで管理します。
どう運用するかはいくつか方法があって、それによって移行の仕方が若干変わってきます。
タイトルなどのメタデータを記事内に書いておき、それを元に検索して asset を取得する(記事とリンクさせない)方法と、
記事に asset へのリンクを貼って、記事の取得時に asset の情報も一緒に取得する方法があります。
ひとつの記事内で複数の asset を扱う場合、どちらにしても Markdown 内にファイルのパスや名前などを書くことになると思います。
また、asset を登録するさいに contentType
というのを求められるので file-type を使います。
yarn add file-type
記事とリンクさせない場合
記事とリンクをしない場合、Media にファイルを移行していくだけです。
注意点として、asset を表示させるときに例えば title をキーとして検索するなら、当然 title はユニークでなければなりません。
ディレクトリ構成が先述のと同じとすると移行コードは次のようになります。
main.ts1import fs from 'fs';2import { join } from 'path';3import filetype from 'file-type';4import { createClient } from 'contentful-management';56const postsDirectory = join(process.cwd(), 'posts');78const getAssets = () => {9const assets: Record<string, string[]> = {};10const slugs = fs.readdirSync(postsDirectory);11for (const slug of slugs) {12const postAssets = fs.readdirSync(join(postsDirectory, slug))13.filter(filename => filename !== 'index.md');14assets[slug] = postAssets;15}16return assets;17};1819const client = createClient({20accessToken: '<access-token>'21});2223const main async () => {24const space = await client.getSpace('<space-id>');25const env = await space.getEnvironment('<environment-id>');2627const assets = getAssets();28for (const [slug, assetName] of Object.entries(assets)) {29try {30const path = join(postDirectory, slug, assetName);31const type = await filetype.fromFile(path);32const asset = await env.createAssetFromFiles({33fields: {34title: {35ja: assetName36},37description: {38ja: ''39},40file: {41ja: {42contentType: type.mime, // image/png など43fileName: assetName,44file: fs.createReadStream(path), // 変数代入ではなく直接渡さないとうまく動きませんでした45}46},47}48});49const processedAsset = await asset.processForLocale('ja'); // 画像をプロセスする50const publishedAsset = await processedAsset.publish(); // 画像を publish する51console.log(`\u001b[32m✅ Updated successfully: ${publishedAsset.fields.title}\u001b[0m`);52} catch (e) {53console.log(`\u001b[31m❌ Update failed: ${assetName}\u001b[0m`);54console.log(e);55}56}57};5859main();
記事とリンクさせる場合
記事に Media というタイプの field を作成しておきます。
記事を作成するさいにその field に特定の形のデータを指定します。
以下は記事と一緒に作ってしまう例ですが、記事がすでに移行済の場合は API から取得した記事を使うことになります。
main.ts1const main async () => {2const space = await client.getSpace('<space-id>');3const env = await space.getEnvironment('<environment-id>');45const posts = getPosts();6for (const post of posts) {7const assetsForEntry = [];8for (const assetName of assets[post.slug]) {9try {10const path = join(postDirectory, slug, assetName);11...12assetsForEntry.push({13sys: {14type: 'Link',15linkType: 'Asset',16id: publishedAsset.sys.id,17}18});19} catch (e) {20...21}22}23post.assets = assetsForEntry;24try {25const entry = await env.createEntry('<content-type-id>', {26fields: Object.fromEntries(27Object.entries(post).map(([k, v]) => [k, { ja: v }])28)29});30console.log(`\u001b[32m✅ Updated successfully: ${entry.fields.slug.ja}\u001b[0m`);31} catch (e) {32console.log(`\u001b[31m❌ Update failed: ${post.slug}\u001b[0m`);33console.log(e);34}35}36};
これでうまくいけば移行したコンテンツが Contentful のダッシュボードに表示されるはずです。
この記事が参考になればうれしいです。
では