Open3

Astro+Contentfulでブログ用独自エディター作ってリアルタイムプレビューしたい

eyemono.moeeyemono.moe

動機

  • 個人ブログをastroで立てた
  • 現在はgitベースでコンテンツ(markdownと添付画像)を管理しているが、適当なCMSに置くようにしたい
    • 画像/動画ファイルをgitに置くことに抵抗がある

    • 新しいブログ記事を書く際に、

      1. ローカルに新規ファイルを作成
      2. プレビュー用にpnpm devでローカルで動かす
      3. 記事が完成したらpush

      の手順を踏むのが面倒

      • 理想的には、ブログにadminページを作って、ブラウザ上で執筆, プレビュー, 投稿ができると嬉しい

Contentfulなどのheadless CMSにコンテンツを置いて、ブラウザ上でコンテンツの編集, 保存をできるようにしたい。

要件

  • ブラウザ上でコンテンツを作成・編集・削除(?)できる
    • Monaco Editorのようなエディタが使いたい
    • 画像のDnDアップロードが使いたい
    • clipboadにコピーした画像をアップロードできる
  • ブラウザ上でコンテンツをプレビューできる
    • リアルタイムプレビューだとなお良い
  • postsのpublished状態が変化したら、自動でビルドが走る
  • ブログ本体はこれまで通りSSGして、記事管理ページだけCSRあるいはSSGする

特に重視しているのが「Monaco Editorのようなエディタが使いたい」の部分。VSCodeでのMarkdown編集に慣れすぎて、Contentful標準のエディタで書くのが辛い。
「じゃあローカルでVSCode使って書けよ」と言われると、「gitに画像置いたりいちいちpushするのが嫌じゃ~~」となる。わがまま。

eyemono.moeeyemono.moe

調査

Contentful Monacoeditor 検索 🔍

それっぽい検索結果は出てこない。こんなにわがままなのは俺だけらしい。

むしろUI ExtensionsでよりWYSIWYGなエディターにしている方がいらっしゃった。

https://dev.classmethod.jp/articles/contentful-ui-extension-editor/

UI Extensions

https://www.contentful.com/developers/docs/extensibility/ui-extensions/

Contentful側に独自のエディタを組み込む方法があるらしい。これでContentful側にMonaco Editorを入れるのもアリか?
ただリアルタイムプレビューのことを考えると、現状.astroファイルで作っているブログ側のデザインを、UI Extensions側に持っていくのがめんどくさそう。

UI Extensionsの利用は一旦保留。

ContentfulのコンテンツをローカルのVSCodeで編集する

https://blog.datsukan.me/create-contentful-markdown-article-cli/

「Contentful側のコンテンツをローカルに同期して、ローカルで好きなエディタ使って編集しちゃおうぜ」の発想。なるほど頭がいい。

ただ今回の要件的には微妙かも(結局プレビューのためにローカルで動かす必要がある, 画像/動画ファイルなどの同期ができるかちゃんと実装読めてなくて不明)。

他のCMSの検討

Contentful以外のCMSのエディタがどんな感じかもざっと調べてみる。MonacoEditor使えるCMSがあるかもしれない。

結果:無さそう

多くのCMSでカスタムプラグインみたいな形で自作エディタを使用できるようになっていたが、結局ContentfulでのUI Extensionsと同じ理由でめんどくさそう。

ただ、Front Matter CMSはかなり面白いなと思った。

https://frontmatter.codes
https://n-e-r-d.jp/notes/front-matter-cms/
https://zenn.dev/naopoyo/articles/front-matter-cms-for-zenn

エディタ自作が無理そうだったらこれを採用したい。

OAuthアプリケーションとしてブログ側にContentfulエディタを実装

https://www.contentful.com/developers/docs/extensibility/oauth/

ContentfulではOAuth2.0がサポートされている。じゃあOAuthアプリケーションとしてブログのadminページにエディタを作れば良さそう。

以下のようなAuth.jsプロバイダーを作ることでContentfulのOAuthを利用することができた。

import type { OAuthConfig, OAuthUserConfig } from "@auth/core/providers";

// https://www.contentful.com/developers/docs/references/user-management-api/#/reference/users
interface ContentfulProfile {
  firstName: string;
  lastName: string;
  avatarUrl: string;
  email: string;
  activated: boolean;
  signInCount: number;
  confirmed: boolean;
  "2faEnabled": boolean;
  sys: {
    type: string;
    id: string;
    version: number;
    createdAt: string;
    updatedAt: string;
  };
}

export const ContentfulProvider = (
  config: OAuthUserConfig<ContentfulProfile> & {
    redirectUri: string;
  },
): OAuthConfig<ContentfulProfile> => {
  return {
    id: "contentful",
    name: "Contentful",
    type: "oauth",
    authorization: {
      url: "https://be.contentful.com/oauth/authorize",
      params: {
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: "content_management_manage",
      },
    },
    token: {
      url: "https://be.contentful.com/oauth/token",
      params: {
        redirect_uri: config.redirectUri,
      },
    },
    userinfo: "https://api.contentful.com/users/me",
    profile: (profile) => ({
      id: profile.sys.id,
      email: profile.email,
      name: `${profile.firstName} ${profile.lastName}`,
      image: profile.avatarUrl,
    }),
    options: config,
  };
};

後はGET /spaces/{spaceId}/users/{userId}で特定spaceにそのユーザーがいるか確認して、確認出来たらadminページを開ける形にすれば良さそう。

https://www.contentful.com/developers/docs/references/user-management-api/#get-a-single-user

eyemono.moeeyemono.moe

インターフェース設計

まず必要なインターフェースを考える。
今後Contentful以外のCMSを使う可能性もあるので、Contentfulへの依存部分は最小限にしたい。

まず本ブログで扱うコンテンツは以下の二種類

  • 本文のMarkdownテキスト
  • 本文で参照される画像/動画ファイルなどのasset

記事のタイトルやタグ, 公開/非公開状態はMarkdownのfrontmatterとして持つようにする。
Contentfulにはpublish/draftの概念があるが、他のCMSにこれがあるかわからないため、記事の内容/状態はできる限りMarkdown内で持つようにしたい。
流石にcreatedAtとupdatedAtはどのCMSでも取得できるはず...

とりあえず以下があれば十分そう。

  • createPost
    • draft状態の空の記事を作成する
    • params
      • slug
  • getPosts
    • return
      • slug
      • createdAt
      • updatedAt
  • getPost
    • params
      • slug
    • return
      • slug
      • content
      • createdAt
      • updatedAt
  • updatePost
    • params
      • slug
      • content
  • uploadFile
    • params
      • file
    • return
      • url

TODO

  • Contentful apiの調査
  • コンテンツ一覧の取得
  • コンテンツ編集ページの実装