0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#183 CloudflareにDecapCMSをデプロイする構築例

0
Posted at

はじめに

最近、様々なCMSツールを探している中で様々な制約で導入を断念することが多い今日この頃です。
そんな中で DecapCMS という面白そうなツールを見つけたので実際に使ってみました。


DecapCMS:https://decapcms.org/

選定理由

まず、今回DecapCMSを選定したのには以下のような理由がありました。
似たような課題を感じている方にとって、DecapCMSは魅力的な選択肢の1つになると思っています。

エディタとしての責務を委譲できる

DecapCMSはコンテンツをGit(GitHub)で管理する特性上、エディタとしての責務のみを持たせるという運用が可能です。
正直、エディタや認証の機能を持たせたアプリを1から自作するのは楽ではないのでこの責務を委譲できるだけでも非常に助かりました。

GitHub Actionsが吸収層として非常に優秀

今回はデプロイ先をCloudflareとしていますが、今後デプロイ先や構成が変わるというのはある程度考えられるケースです。
そういった中で、GitHub Actions を用いると、1次データをDecapCMSからのコミットをトリガーとした上で「デプロイ先や構成に合った形で加工する」役割を担わせることが可能です。

移行に強い

上記で挙げたように責務が明確化しているため、DecapCMSを今後デプロイ先の構成に特化させた自作アプリで代替して運用していく場合でも、エディター部分(DecapCMS)とロジック部分(GitHub Actions)を代替することで比較的簡単に移行が可能だと考えています。

前提

本記事は以下の要素が登場します。

  • デプロイ先:Cloudflare
  • 言語:Typescript
  • FW:Hono
  • 認証:GitHub OAuth

また、本記事では構築~デプロイまでを目的としてDecapCMSの細かな設定などには触れませんのでご了承ください。

プロジェクトの構成

本記事で構築するプロジェクトの主な全体構成は以下のようになります。

cms/
  ├ public/ # 静的コンテンツ配置場所
  │  └ admin/
  │    └ index.html # DecapCMSのエントリ
  ├ src/
  │  ├ handler/
  │  │  ├ admin/
  │  │  │  └ configHandler.ts # DecapCMSで使うConfig.ymlを環境毎に変更するエンドポイント
  │  │  └ auth/
  │  │     ├ authHandler.ts # GitHub認証をするためのエンドポイント
  │  │     └ callbackHandler.ts # GitHub認証後、Tokenを取得するエンドポイント
  │  └ index.ts
  ├ .dev.vars
  ...

アプリケーションの作成

プロジェクトの初期構築

初期構築には以下コマンドを使用し、問答は以下のように選択しました。

pnpm create cloudflare@latest cms

What would you like to start with?
> ● Framework Starter
> ● Hono

You're in an existing git repository. Do you want to use git for version control?
> Yes

> Do you want to deploy your application?
> No

上記のようにすると、Honoを使用したテンプレートが作成されるので作成されたテンプレートを元に開発を進めます。

環境変数ファイルの作成

開発に入る前に、先に環境変数の定義ファイルだけ作成しておきます。


.dev.vars を作成し、以下の内容で保存してください。

text
APP_URL=""http://localhost:8787""
GITHUB_CLIENT_ID=""後で記述""
GITHUB_SECRET_ID=""後で記述""

上記のように作成出来たら以下コマンドを実行して型ファイルを更新します。

pnpm run cf-typegen

DecapCMSのエントリを作成

手順については 公式のインストールガイド を参考に進めます。

public 配下に admin/index.html を作成し、以下の内容で保存します。

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset=""utf-8"" />
    <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
    <meta name=""robots"" content=""noindex"" />
    <title>CMS Manager</title>
  </head>
  <body>
    <script src=""https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js""></script>
  </body>
</html>

上記ファイル作成後、公式の手順では同階層に config.yml を設置し設定を記述しますが、環境毎に設定値を変えたいなどの場合に備えて今回はエンドポイントからConfigを返すようにします。


まずは必要なパッケージをインストールします。

pnpm add js-yaml && pnpm add -D @types/js-yaml

次に src 配下に handler/admin/configHandler.ts を作成し、以下の内容で保存します。

ts
import { Context } from ""hono"";
import * as yaml from ""js-yaml"";

export async function configHandler(
  c: Context<{ Bindings: CloudflareBindings }>
) {
  const config = {
    backend: {
      name: ""github"",
      branch: ""main"", // コンテンツ保存先のブランチ名
      site_domain: c.env.APP_URL,
      base_url: c.env.APP_URL,
      auth_endpoint: ""/auth"", // 認証用の自作エンドポイント
      repo: ""cms"", // 自身のリポジトリに置き換える
    },
    media_folder: ""public/content/media"", // 画像の保存先(フォルダ)
    public_folder: ""public/content/media"",
    collections: [
      {
        name: ""blog"",
        label: ""Blog"",
        folder: ""public/content/blog"", // 記事の保存先(フォルダ)
        create: true,
        slug: ""{{year}}-{{month}}-{{day}}-{{slug}}"",
        fields: [
          { label: ""Title"", name: ""title"", widget: ""string"" },
          { label: ""Publish Date"", name: ""date"", widget: ""datetime"" },
          {
            label: ""Featured Image"",
            name: ""thumbnail"",
            widget: ""image"",
            required: false,
          },
          { label: ""Body"", name: ""body"", widget: ""markdown"" },
        ],
      },
    ],
  };
  return c.text(yaml.dump(config), {
    headers: {
      ""Content-Type"": ""text/yaml"",
      ""Cache-Control"": ""no-store"",
    },
  });
}

上記のようにすることで、環境毎に環境変数で設定を切り替えたりすることが可能になります。
作成できたら、Handlerを src/index.ts にルーティングに登録します。

index.ts
import { Hono } from ""hono"";
+ import { configHandler } from ""./handler/admin/configHandler"";

const app = new Hono<{ Bindings: CloudflareBindings }>();

- app.get(""/message"", (c) => {
-   return c.text(""Hello Hono!"");
- });

+ app.get(""/admin/config.yml"", configHandler);

export default app;

上記まで作成できたら一旦以下コマンドでデプロイを行い、発行URL + /admin で以下画面が表示されるか確かめて見ると良いと思います。

pnpm run deploy

GitHub認証のエンドポイントを実装

DecapCMSはNetlifyへのデプロイでは簡単にGitHub認証を行ってくれたりするみたいなのですが、Cloudflareなどでは自身でエンドポイントを実装してあげる必要があるようです。


上記の理由から、公式でも紹介されているテンプレート を参考に認証用のエンドポイントも自作します。

/auth

GitHubのOAuth認証へリダイレクトするためのエンドポイントを実装します。


src/handler/auth/authHandler.ts を作成し、以下の内容で保存してください。

ts
import { Context } from ""hono"";

export async function authHandler(
  c: Context<{ Bindings: CloudflareBindings }>
) {
  const redirectUrl = new URL(""https://github.com/login/oauth/authorize"");
  const state = crypto.randomUUID();

  redirectUrl.searchParams.set(""client_id"", c.env.GITHUB_CLIENT_ID);
  redirectUrl.searchParams.set(
    ""redirect_uri"",
    `${c.env.APP_URL}/auth/callback`
  );
  redirectUrl.searchParams.set(""scope"", ""repo"");
  redirectUrl.searchParams.set(""state"", state);

  return c.redirect(redirectUrl, 301);
}

先ほど同様に、 src/index.ts へのルーティングを登録します。

index.ts
import { Hono } from ""hono"";
+ import { authHandler } from ""./handler/auth/authHandler"";
import { configHandler } from ""./handler/admin/configHandler"";

const app = new Hono<{ Bindings: CloudflareBindings }>();

app.get(""/admin/config.yml"", configHandler);
+ app.get(""/auth"", authHandler);

export default app;

こうすることで後ほど作成する GitHub OAuth クライアントを用いて認証を行うことができます。

/auth/callback

認証用のエンドポイントを作成したことで、Codeを持った状態で /auth/callback へ戻ってくるようになりました。
これを利用して次はCodeを受け取り、Tokenを取得するエンドポイントを実装します。


src/handler/auth/callbackHandler.ts を作成し、以下の内容で保存してください。

ts
import { Context } from ""hono"";

export async function callbackHandler(
  c: Context<{ Bindings: CloudflareBindings }>
) {
  const code = c.req.query(""code"");
  if (!code) {
    return c.body(""不正なcodeです"", 400);
  }

  const token = await fetchGitHubToken({
    clientId: c.env.GITHUB_CLIENT_ID,
    clientSecret: c.env.GITHUB_SECRET_ID,
    code: code,
  });
  if (!token) {
    return c.body(""tokenの取得に失敗しました"", 400);
  }

  return c.html(`
    <script>
      const receiveMessage = (message) => {
        window.opener.postMessage(
          'authorization:github:success:${JSON.stringify({
            token: token,
            provider: ""github"",
          })}',
          message.origin
        );
        window.removeEventListener(""message"", receiveMessage, false);
      }
      window.addEventListener(""message"", receiveMessage, false);
      window.opener.postMessage(""authorizing:github"", ""*"");
    </script>
    `);
}

async function fetchGitHubToken({
  clientId,
  clientSecret,
  code,
}: {
  clientId: string;
  clientSecret: string;
  code: string;
}): Promise<string | null> {
  const response = await fetch(""https://github.com/login/oauth/access_token"", {
    method: ""POST"",
    headers: {
      ""content-type"": ""application/json"",
      ""user-agent"": ""cms"",
      accept: ""application/json"",
    },
    body: JSON.stringify({
      client_id: clientId,
      client_secret: clientSecret,
      code: code,
    }),
  });

  if (response.ok) {
    const result = (await response.json()) as { access_token: string };
    return result.access_token;
  } else {
    return null;
  }
}

上記実装では主に以下の事を行っています。

  • fetchGitHubToken() でCodeを用いてTokenを取得
  • 取得したTokenを親ウィンドウに送信するスクリプトを返却

こうすることで、取得したTokenがDecapCMSへ渡されログイン状態となります。


作成出来たら src/index.ts へルーティング登録を行います。

index.ts
import { Hono } from ""hono"";
import { authHandler } from ""./handler/auth/authHandler"";
+ import { callbackHandler } from ""./handler/auth/callbackHandler"";
import { configHandler } from ""./handler/admin/configHandler"";

const app = new Hono<{ Bindings: CloudflareBindings }>();

app.get(""/admin/config.yml"", configHandler);
app.get(""/auth"", authHandler);
+ app.get(""/auth/callback"", callbackHandler);

export default app;

ここまででGitHub認証の基盤はできました。

GitHub OAuth Appsの作成

アプリ側の実装はできたので、次はGitHub側の作業として、実際にOAuthに必要なAppsの作成と環境変数への記入を行います。


GitHub developers から OAuthApps を選択し、以下内容を記述して作成します。

  • Application name:好きに付けてOK
  • Homnepage URL:アプリケーションのURL
  • Authorization callback URL:作成したcallbackエンドポイントへのURL

上記で作成が行えたら画面内に表示される Client IDClient secrets(Generateボタンで作成) をコピーしてメモしておきます。


ここまででそれぞれの情報が揃いました。
最初に作成した .dev.vars に先ほどメモした内容を記入して保存します。

text
APP_URL=""http://localhost:8787""
GITHUB_CLIENT_ID=""メモしたClient ID""
GITHUB_SECRET_ID=""メモしたClient secrets""

動作確認

保存したらアプリケーションを pnpm dev で立ち上げ、 localhost:8787/admin にアクセスして Login with GitHub をクリックして認証を行います。


認証後、以下のような画面が表示されれば成功です。

ここまでくれば New Blog などで実際に記事を作成し、コンテンツがリポジトリに保存されていることを確認してみてください。

デプロイ

上記設定さえ終わっていれば、後はデプロイを行い以下の内容を実施することで本番でも同様の動作が可能です。

一旦先にデプロイを行っておき、ダッシュボード上で環境変数などの設定を行います。

pnpm run deploy

GitHub OAuth Appsの作成

繰り返しになるので割愛しますが、ローカル同様に本番用のクライアントも作成します。
設定値は本番で払いだされたURLになるのでそこだけ注意します。

環境変数の設定

Cloudflareのダッシュボードからデプロイ済みのWorkersを選択し、 設定 > 変数とシークレット からローカルで設定した内容と同様のキー名で値を先ほど作成した本番用クライアントの内容で記述します。


ここまで設定出来たらローカル同様、動作確認を行いローカルの期待値と同様、ダッシュボードが表示されれば完了です!

おわりに

冒頭でもお話した通り、ずっとCMSツールについて模索している中で現状とても満足できるツールが見つかったので一旦これで運用してみる予定です。

また、本格的なブログの運用をしようとすると検索などの観点から少し弱い部分もでてきそうなので、今後は冒頭に挙げた吸収層として、D1やR2へデータを退避するWorkflowなどを作って見たいと思っています。
他にも、デスクトップアプリケーション化による恩恵や、画像周りの最適化などを試みても面白そうだと感じているので、その辺りを試してみたらまた記事にしてみたいです。

少し長くなりましたが、ここまで読んでいただきありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?