🔖

Zenn の記事を Headless CMS(今回は microCMS を利用) で作成する

2021/12/26に公開

Zenn を使うにあたり、記事の作成を HeadlessCMS(今回は microCMS を利用) + 自作のツール(remote-cms-content)で行うことにしました。

この記事はその検証も兼ねて作成したものです。

構成

まだテスト段階で手動による実行部分も多いので、簡単な構成のみ記述します。

編集とプレビュー

以下のように記事(article)用 API を作成しリッチエディタ記述、ある程度まとまったら保存用スクリプトを実行しローカルにダウンロードします(手動で実行)。

保存用スクリプトで利用している rcc コマンドはマッピング設定を用意することで、受信データを FrontMatter + Markdown へ変換する機能を持っています。

変換機能により Zenn 形式で保存されたファイルは zenn-clitextlint でプレビューとチェックが行えます(チェック結果の確認はスクリプトの出力を目視で行う)。

スクリーンショットとマッピング設定。

article API のスキーマを表示しているスクリーンショットarticle API のスキーマ

リッチエディタでコンテンツを編集中しているスクリーンショットリッチエディタでの編集

保存スクリプトを実行して textlint のエラーが表示されているスクリーンショット保存用スクリプトの実行例

マッピング設定
disableBaseFlds: true
passthruUnmapped: false
selectFldsToFetch: true
position:
  disable: true

media:
  image:
    library:
      - src: https://images.microcms-assets.io/assets/
        kind: imgix

flds:
  - fetchFld: title
    query: title
    dstName: title
    fldType: string

  - fetchFld: content
    query: content
    dstName: content
    fldType: html
    convert: markdown
    toMarkdownOpts:
      imageSalt:
        command: rebuild
        baseURL: https://images.microcms-assets.io/assets/
        rebuild:
          keepBaseURL: true
          baseAttrs: 'data-salt-qm="auto=compress,format"'
      unusualSpaceChars: throw

  - fetchFld: emoji
    query: emoji
    dstName: emoji
    fldType: string

  - fetchFld: type
    query: type
    dstName: type
    fldType: string

  - fetchFld: topics
    query: '[topics.topic]'
    dstName: topics
    fldType: object

  - fetchFld: published
    query: published
    dstName: published
    fldType: boolean

Zenn へのデプロイ

保存用スクリプトは「publishedtrue でなおかつ textlint をパスチェックしたファイル」を GitHub へプッシュするためのリポジトリへコピーします。

コピーされたファイルが問題なさそうであれば GitHub CLI で GitHub のリポジトリを更新します(ここも目視で確認、手動で実行)。

メモ機能

記事とは別にメモ用の API を作成することで、参考にしたページなどをメモしておくことができます。

また、メモは参照フィールドを使うことで記事からリンクさせることもできます(項目を自由に増やしたりリンクできるのは Headless CMS ならではかと)。

スクリーンショット。

memo API のスキーマを表示しているスクリーンショットmemo API のスキーマ

記事からメモへリンクしているスクリーンショット記事からメモへのリンク

実際に記述してみたもの

「Zenn の Markdown 記法」を上記構成で記述したサンプルです。

基本的にはリッチエディタの HTML 出力を rehype-remark で変換し、リッチエディタで表現できない部分はコードブロックを独自記法で拡張すること等により対応しています。

画像

リッチエディタの通常の UI 操作で挿入し、サイズ変更や alt とリンクを設定できます。

Markdown に比べると煩雑なように思えますが、エスケープなどはエディター側が処理してくれるので(気分的には)楽に感じます。

画像の設定をしているスクリーンショット

曇り空な河川敷

画像(imgix)

microCMS の画像はバックエンドが imgix なのでクエリーパラメーターで加工もできます。

ただし、通常の UI では行えないので、rehype-image-salt記法で設定します(この辺は remote-cms-content による Markdown 変換時に処理されます)。

rehype-image-salt の記法でクエリーパラメーターを設定しているスクリーンショット

曇り空な河川敷

画像(Zenn)

Zenn 独自の記法ではキャプション風の表示は以下の方法で対応できます。

  • 画像の直後にイタリックでテキスト入力する
  • rehype-image-salt の data-salt-zenn-cap を利用する

ただし、厳密には正規の指定と若干異なる表記になるので(本来は画像の下に記述すべき)、将来的に使えなくなる可能性はあります。

画像のすぐ下の行に*で挟んだテキストを配置すると、キャプションのような見た目で表示されます。

また、横幅の指定は対応できていません。

イタリックにしたテキストによるZennのキャプション風表示を指定しているスクリーンショット

曇り空な河川敷Zenn の Caption 風表示

コードブロック

コードブロックで言語やファイル名を指定する場合は拡張されたコードブロック内でコードブロックを記述する必要があります(ややこしいですが、慣れるとあまり気になりません)。

なお、リッチエディタでは書式なし(いわゆるプレーンテキスト)で連続する空白文字をコードブロックへペーストすると No-Break Space になってしまいます。

テキストエディターからペーストするとき気になる場合は対策が必要です(remote-cms-content では「警告を出す」「変換する」ことで対応しています)。

また、(コードブロックに限った話ではないですが) & のエスケープ処理が独特なので > などを記述することが難しいです(ここではいわゆる全角で & を記述することで回避しています)。

参考: micoCMS のメモとサンプル - & のエスケープ

拡張されたコードブロック内に記述しているスクリーンショット

fooBar.js
const great = () => {
  console.log("Awesome")
}

引用

リッチエディタの通常の UI で指定できます。ただしペーストするときの書式の有無、改行や空白の扱いなどに注意は必要です。

リッチエディタのUIで引用を指定しているスクリーンショット

引用文。 引用文。

注釈

注釈は[] がエスケープされてしまうので、下線を利用する機能を追加しました。

  1. ^ 付きの文字を下線ありにすると footnoteReference へ変換([] は記述しない)

  2. それ以外の下線ありの文字列は footnote(インライン注釈)へ変換(^[] は記述しない)

    • インライン注釈内では Markdown で記述可能(リンクなどを設定できる)。

リッチエディタの UI で下線を指定し脚注を記述しているスクリーンショット

脚注の例[1]です。インライン[2]で書くこともできます。

テーブル

拡張されたコードブロック内で記述できます。

拡張されたコードブロック内にテーブルを記述しているスクリーンショット

Head Head Head
Text Text Text
Text Text Text

メッセージとアコーディオン(トグル)

::: はそのまま記述できるので、Markdown へ変換後に改行されるようにすれば記述できます。

通常の段落に ":::" を記述してメッセージとしているスクリーンショット

タイトル。

表示したい内容。装飾なども指定可能。

数式

これも $$ をそのまま記述できるので、とくに変わったことはしなくても記述できます。

ただし、(KaTex の記法にうといので影響度は不明ですが)Markdown 的な記法があるとリッチエディタの機能で変換される可能性はあります。

通常の段落に "$$" を記述して数式としているスクリーンショット

e^{i\theta} = \cos\theta + i\sin\theta

インラインの場合 a\ne0 も記述できます。

コンテンツの埋め込み

remark-gfm の autolink とリッチエディタの変換が効いてしまうので、拡張されたコードブロック内で @[card](URL) 等の記法を使う必要があります。

拡張されたコードブロック内にカードのリンクを記述しているスクリーンショット

ダイアグラム

拡張されたコードブロック内で記述できます。

拡張されたコードブロック内にダイアグラムを記述しているスクリーンショット

モバイル対応

記事はデスクトップでないと厳しい面も多いのですが、「メモは思いついたときにちゃちゃっと入力したい」というのがあったのでわりと重視した項目です。

以下のスクリーンショットはジョギング中に思いついたこをメモしたものです。

スクリーンショット。

スマートフォンでメモを作成したときのスクリーンショット

ジョギング中にいつもリッチエディタでメモをとっているので慣れもありますが、簡単なメモならそれなりに入力できます。

ちなみに、各種 Headless CMS サービスの中から microCMS を選択した理由の 1 つがこれです。

「レスポンシブ対応」「スマートフォンでも RichText 系フィールドへの日本語入力が安定している」サービスとなると、現状では選択肢が限られます。

各種 Headless CMS 対応

今回は microCMS を利用してますが、remote-cms-content が対応しているサービスであれば同じように構成できます。

たとえば、GraphCMS では以下のような GraphQL のクエリーとマッピング設定を用意することで対応できます(実は当初 GraphCMS で試していた)。

参考設定(内容的にはちょっと古いです)。
クエリー
query GetContent($stage:Stage!, $endCursor: String, $pageSize: Int) {
  articlesConnection(stage:$stage, after:$endCursor, first:$pageSize){
    edges{
      node{
        id
        slug
        title
        content {
          html
        }
        emoji
        type
        topics
        published
      }
    }
    pageInfo{
      hasNextPage
      endCursor
    }
  }
}
マッピング設定
disableBaseFlds: true
passthruUnmapped: false
position:
  disable: true

transform: data.articlesConnection.{"items":[edges.node],"pageInfo":pageInfo}

flds:
  - query: slug
    dstName: id
    fldType: id

  - query: title
    dstName: title
    fldType: string

  - query: content.html
    dstName: content
    fldType: html
    convert: markdown
    toMarkdownOpts:
      imageSalt:
        command: "rebuild"
        baseURL: https://media.graphcms.com/
        rebuild:
          keepBaseURL: true

  - query: emoji
    dstName: emoji
    fldType: string

  - query: type
    dstName: type
    fldType: string

  - query: topics
    dstName: topics
    fldType: object

  - query: published
    dstName: published
    fldType: boolean

各サービスの機能は異なるので全く同じにはできませんが、逆にいえばサービス別の設定を用意しておき「記事の特性にあわせて利用するサービスを切り替える」ということもできます。

Markdown 系フィールドは?

サービスによっては Markdown 入力に特化したフィールドが用意されています。

Contentful であれば「(デスクトップ環境で)日本語入力が普通に利用できる」「フルスクリーンにできる」「コードブロックも記述できる」など利点も多いです。

また、今回の利用方法であれば「GitHub の変更も取り込みやすい」という点も見逃せません。

しかし、(個人的な好みもありますが)以下の点で RichText 系フィールドを利用しています。

  • Markdown フィールドへアセット(画像) を挿入すると参照関係にならない

「記事のエントリー」と「アセット(画像)」が参照関係にならないことには 2 つのデメリットがあります。

  • アセット(画像)の変更が Markdown フィールドに反映されない
  • アセット(画像)を利用しているエントリーを把握できない

写真などを主に扱う場合は影響が少ないのですが(画像 API で加工できることが多い)、スクリーンショットなどを扱う種類のテキストでは画像を入れ替えることも多いので操作性へ影響してきます。

以上のことから現状では Markdown 系フィールドの利用を見送っています。

本の作成

記事の作成とは直接の関係はないのでさわりだけ。

「設定」と「チャプター」のモデル(API) を作成し参照関係を構成することで本の作成(CLI 管理形式での保存)もできます。

スクリーンショット。

本の設定からチャプターの参照している状況のスクリーンショット本の設定からチャプターを参照

本のプレビューを表示しているスクリーンショット本のプレビュー

検討事項

いくつか記事を作成してみたところ良い感じではあるのですが、やはり検討すべき項目もあります。

プレビューとチェック

現在の構成では「リッチエディタ」「ターミナル」「プレビュー」用の画面を行ったり来たりするのでどうにかしたいところです。

一番よさそうなのは、draftlint のときのように「リッチエディタの画面プレビューボタンをクリックするとチェック結果付きのプレビューが表示される」なのですが、ちょっと大変そうかなと。

publish の自動化

これもローカル側での手動部分が多いので、CMS 側から直接 GitHub Actions を起動して自動化する方がよいのかなと。

ただ、感覚的な問題なのですが、自動化(ブラックボックス化)しすぎてしまうのも味気ないかなというのもあって少し考え中です。

CMS と Zenn の publish

現在はスキーマ内に Zenn 用の publish フィールドを定義しています。

本来なら CMS の publish にあわせてるべきですが、各 CMS でプレビューの扱いが異なるのでこれも対応方法を考え中です。

メモ

プレビューアプリ

メモの中で mermaid によるダイアグラムを書きたくなることがあり、そうなるとメモにもプレビュー的な表示が欲しくなってきます。

Scraps との使い分け

Zenn Scraps を少し使ってみたとろ「メモはこれでもいいかな」と少しぐらついてしまっている。

情報が分散してしまう

普段は howm(qfix-howm)でメモをとっているので情報が分散してしまいます。

これについては、howm の中に zenn ディレクトリーを作ってダウンロードしてしまおうかと考えています。

どちらも Markdown なので、1 箇所にまとめてしまえば検索などは違和感なく使えるかなと。

プルリクエスト

以下の記事を参考にしてリポジトリの名前を変更しているときに「現状の構成だとプルリクエストなどをいただいても Headless CMS 側へ反映させる方法がない」ということに気が付きました。

編集履歴

各 CMS の無料プランでは履歴管理はだいたいが対象外なのですが、今回はローカル側のリポジトリにダウンロードするのでそちら対応できないか考え中です。

Vim キー配列

mermaid の記述をするときなどにテキストオブジェクトが使えないのはやはりきびしいなと。

ブラウザー拡張などで解決しそうですが、拡張はあまり入れたくないのでおとなしく普通に編集しています。

emoji

これが意外とめんどうで「ランダムな emoji をどうやって取得する?」「バリデーションで emoji を 1 文字ってどう指定する?」等々。

現状は「zenn-cli で作成した article のファイルからコピペ」することで対応しています。

おわりに

以上のような感じで「記事と関連メモを手軽に編集できる環境」ができあがりました。

いくつか検討事項もありますが、しばらくはこの構成で記事を作成していこうかと。

脚注
  1. 脚注の内容その 1。 ↩︎

  2. 脚注の内容その 2 ↩︎

GitHubで編集を提案

Discussion