今日も今日とてGatsby Contentfulでのアプリ制作の話。

Contentfulで長文入力する二種類の方法

Contentfulでブログの本文のような長文を入力するフィールドは2つある

1.リッチテキスト(wisywigフォーム)形式

1つ目がRich Text方式。たぶんほとんどの人がこちらを使うと思う。

scrのコピー

GUIで各種装飾が行えて、Wordpressのエディタにも近い、ブログに必要なタグはほぼ全て使える。

ただ、なぜか全画面編集ができないというデメリットがあって(2020年6月28日現在)、小さいテキストエディタでせこせこブログを入力しないといけなくて、これはちと不便。

2.マークダウン形式

2つめはマークダウン方式。

ContentfulのContent Modelからフィールドを作成する際、フィールドタイプで
Textを選択 → Long text, full-text searchタイプを選択 → AppearanceでMarkdownを選択
という流れで有効化できる。

見出し(Heading)は1 ~ 3までしか使えないとか、リッチテキストに比べて簡易的ではあるけど必要十分な装飾が可能で、かつこちらでは全画面で編集ができる

Screenshot-2020-06-28-at-14.47.17

左に入力フォーム、右にプレビューを確認しながら編集ができて、実際に近い見た目を確認しながらライティングに集中できるのでこちらのほうが好み。

マークダウン形式の問題点

ただマークダウン方式で困るのは、二行以上の改行がフロント側で表現されないということ。

マークダウンの場合、文章間を一行以上開けると、その段落は独立したパラグラフとして認識されるので、最終的にHTMLに変換される際は、改行タグではなく1つの<p>タグで囲まれてしまう。

まだせめて改行が空の<p>タグになるなら(不格好だけど)入力通りの見た目になるのだけど、改行がいくつあっても1行の改行になってしまう。

これでは改行で文章の合間を調整することができなくなってしまうので、今回はView側でどうにか調整してみた。

関連プラグインのインストール

マークダウン形式のテキストをHTMLに変換できるプラグインmarkedをインストールする。

$ npm install marked

ソースコード

記事の本文を表示するページのテンプレートファイルのコードは大体以下の感じになった。

src/templates/post.js


import marked from "marked";

const BlogArticle = ({ data, pageContext }) => {
  const { title, contentMarkdown, thumbnail, category, createdAt, tags } = data.contentfulBlogArticle;

  const source = contentMarkdown.contentMarkdown.replace(/\n/gi, '\nreplaced_text ');
  marked.setOptions({
    gfm: true,
    breaks: true,
  });
  const parsedSouce = marked(source).replace(/replaced_text/g, '');

  return (
<Layout>
  <div className="container flex-row">
    <div className="main">
      <div className="post">
        <h1>{title}</h1>
        <p>カテゴリ: {category}</p>
        <p>投稿日: {createdAt}</p>
        <Img
            fluid={useContentfulImage(thumbnail.file.url)}
        />
        <div className="body-text" dangerouslySetInnerHTML={{ __html: marked(parsedSouce) }} />
      </div>
    </div>
    <Sidebar />
  </div>
</Layout>
  );
};
export default BlogArticle;
export const pageQuery = graphql`
  query( $slug: String, $tags: [String] ) {
    contentfulBlogArticle(slug: { eq: $slug }) {
      id
      title
      category
      contentMarkdown{
        contentMarkdown
      }
      thumbnail {
        file {
          url
        }
      }
      createdAt(formatString: "YYYY-MM-DD")
    }
  }
`;

部分ごとの解説

まずmarkedプラグインのimport。

import marked from "marked";

次にマークダウンからHTMLへの変換部分。ここが肝。

const source = contentMarkdown.contentMarkdown.replace(/\n/gi, '\nreplaced_text ');

marked.setOptions({
        gfm: true,
        breaks: true,
});

const parsedSouce = marked(source).replace(/replaced_text/g, '');

変数souceで、Contentfulで入力されたテキストを取得して、本文中の改行コード\n\nreplaced_text(改行コード 置換文字 半角スペース)に置き換えている。

markedプラグインではオプションが設定できるので(公式サイト)、改行タグを挿入するオプションを有効化している。gfmも一緒にtrueにしてあげないと有効化されないので注意。

変数parsedSouceでは、marked関数でHTMLに変換した後に、置換文字(replaced_text)を削除している。

これで前後の文に影響を及ぼすことなく、複数行改行が<br>に置き換わるはず。

最後に、出力部分ではHTMLタグがそのまま出力されないようにdangerouslySetInnerHTMLを使ってhtmlオブジェクトを出力する。

<div className="body-text" dangerouslySetInnerHTML={{ __html: marked(parsedSouce) }} />

dangerouslySetInnerHTMLはクロスサイトスクリプティング (XSS) 攻撃のリスクを回避するためのReactの機能の一つらしい(React.jsのサイト)。

この方法の注意点

改行タグがすべて維持されるため、各パラグラフがpタグで囲われなくなる点に注意が必要。<p>タグでmarginなどうまく取っている場合はレイアウトがいつもと少し変わるかも。

さいごに

数時間悩んだあげくに上記の方法で着地したんだけど、確実にベストプラクティスはこれじゃない感ある。

賢い方いたら教えてください〜。