📚

【Gatsby + Contentful】 僕のRich Textに救済を

22 min read 6

はじめに

なんか既視感のあるタイトルから始まります、今回の記事。本稿では、「これからGatsbyJs」を使ってブログ・メディアを作ってみたい皆さんに向けて、私がハマって辛かった情報をご提供できればと思います。
 さて、本題に入る前に、ひとつだけ言わせてください。「みんな、もっとContentful + Gatsbyの構成を使って!」 
 …いや、あのですね、情報が少なすぎませんか?どうして浅瀬の民がちょっとしたブログを作るためにGatsby本体のissueをアサリに行かなくてはならないのか。
 あ!そっかぁ、潮干狩りか!え、あ、ちょっまっ…ゴン! イテテテ…

今回の潮干狩り会場のご紹介

では、今回の潮干狩り会場についてご紹介させていただきます(強い意志)。会場となる干潟は以下の要素で構成されております。こちら事前情報程度の簡単なものですので、詳しくは各項目のリンク先をご覧ください。

GatsbyJs

今回の主役。静的サイトジェネレーターってやつですね。ReactをWebサイト制作に転用できるすごいやつ。似たようなものにNext.jsがありますが、こちらのほうがよりサイト制作に特化されており、豊富なPluginでいろいろとよしなにやってくれる。詳しくはこちら

Contentful

HeadlessCMSの一種。いい感じのコンテンツ管理画面を提供してくれる。5000レコードまで無料だって。詳しくはこちら。あと、HeadlessCMSっていうのは「Word Pressの表示画面がない版」って考えてもらえればいいと思います。…知らんけど。

GraphQL

Gatsbyにおいて、データを取得するためのquery 言語。RESTと違って一回でデータを取得しきれるのが偉いらしい。潮だまりよりも浅い筆者の知識を鵜呑みにしてはならない(戒め)。詳しくはこちら

Gatsby Source Contentful

GatsbyJsとContentfulの仲人。これを使うと、Contentfulで管理しているデータをgraphQLから取得することができる。Keyを設定するだけだから簡単だよ。詳しくはこちら。ちなみに、Rich Textの取得の際にもこいつに含まれるrenderRichTextってのを使います。それに関しては当該箇所で詳しく。

Gatsby Plugin Image

Gatsby Imageの正当なる血統を有する後継者。こいつを使うと画像の遅延読み込みとかをいい感じにやってくれる。あと、以前に対してGastby Image Dataなるものから画像のデータを一挙に取得できるので、以前よりも格段に書きやすくなった印象。詳しくはこちら

会場は大変ぬかるんでおりますので、足を取られないよう十分にご注意ください。では早速、今回足を取られたぬかるみについてご紹介していきます。

Contentful + Gatsbyの基本設定

本題に入る前に、Contentful + Gatsbyの基本的な設定について簡単に触れておきます。この部分までは参考となる資料も複数ありますので、私の知識と実装が信頼できないというまともな知性と感性をお持ちの皆様はご自身で調べてみて下さい。
 とりあえず、コードにすると以下のような感じの実装になります。

/* gatsby-node.js */

/* 次に示すtemplateを経由してコンテンツを生成する */
exports.createPages = async ({ graphql, actions, reporter }) => {
 const { createPage } = actions;
 const result = await graphql(
   `
   query {
     allContentfulBlogPost(sort: {order: DESC, fields: createdAt}) {
       edges {
     node {
       finalUrl
       title
       createdAt(formatString: "YYYY/MM/DD")
       description
       eyeCatch {
         gatsbyImageData
       }
       content {
         raw
       }
     }
       }
     }
   }
   `
 );
 
 if (result.errors) {
   reporter.panicOnBuild(`Error while running GraphQL query.`);
   return
 };
 
 const PostTemplate = path.resolve(`./src/templates/PostTemplate.tsx`);
 
 result.data.allContentfulBlogPost.edges.forEach(({ node }) => {
   createPage({
     /* 出力先のpath、利用するcomponent、渡すcontextつまりデータを指定する */
     path: `/services/${node.finalUrl}`,
     component: PostTemplate,
     context: {
       post: node,
     }
   });
 });

では簡単にご説明を。まず、Contentful内部でこんな感じのデータを持つ記事の枠組みを作ります。

そしてこのデータを取り出すためのqueryを上記のように発行します。createdAtの末尾で投稿日を表示するよう成型しています。また、eyeCatchではGatsby Plugin Image用にGatsbyImageDataを、RichTextでは中身がrawに入っているのでそれを取り出します。
そして、それを表示するTemplateはこんな感じ。

/* PostTemplate.tsx */

import React from 'react';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';

const PostTemplate = ({ pageContext }) => {
    const { post } = pageContext;
    const eyeCatch = getImage(post.eyeCatch);
    
    return (
        <>
          <GatsbyImage
            image={eyeCatch}
            alt={`${post.title} eyeCatch`}
          />
          <h1> {post.title} </h1>
	  <p>{post.createdAt}</p>
          {post.content && renderRichText(post.content)}
        </>
    )
};

export default PostTemplate;

最低限ですが、これで要素は表示されるはずです。renderRichTextに引数としてcontentを渡してあげれば、まるっと中身を表示してくれます。Styleを当てたい場合、各々ご利用のライブラリなどでcssをあてていただければと思います。
 ね、簡単でしょ。私もそう思っていました。…質の悪いことに、実際簡単なんですよね、Rich Textをのぞけば

投稿者:「ぬかるんではいるけど、干潟としてはこんなもんでしょ!あ、アサリはっけ~ん♪」

Rich TextにStyleをあてたい

Rich Textは当然htmlで表示されるわけですが、すべてがcontentと名付けられたパンドラの箱の中。…こうなると、CSSってどうやってあてるんでしょうか?それを担当するのがrenderRichTextのoptionsです。
 contentful/rich-text-typesってところ?で各html要素が出力される形が決まっています。それを見ながら、各要素ごとにComponentを作っていく感じでしょうか。正直なところ、これを文章で説明するのは相当大変なので、以下のコードをご覧ください。

/* PostTemplate.tsx */

import React from 'react';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS, MARKS } from '@contentful/rich-text-types';

const options = {
    renderNode: {
        /* <p> 用のコンポーネント */
        [BLOCKS.PARAGRAPH]: ( node, children ) => <SomePComponent content={children} />,
	/* <h1> 用のコンポーネント <h2>以下も同様 */
        [BLOCKS.HEADING_1]: ( node, children ) => <SomeH1Component content={ children } />,
	/* <ul> 用のコンポーネント <ol>もOL_LISTに変更で行けます */
        [BLOCKS.UL_LIST]: (node, children) => <SomeUlComponent content={ children } />,
	/* <blockquote> 用のコンポーネント */
        [BLOCKS.QUOTE]: (node, children) => <SomeQuoteComponent content={ children } />,
	/* 画像表示用のコンポーネント */
        [BLOCKS.EMBEDDED_ASSET]: node => <SomeImageComponent content={ node } />,
	/* 内部リンクブロック表示用のコンポーネント */
        [BLOCKS.EMBEDDED_ENTRY]: node => <SomeEntryBlockComponent content={ node } />,
	/* インライン要素の内部リンク表示用のコンポーネント */
	[INLINES.EMBEDDED_ENTRY]: node => <SomeEntryInlineComponent content={ node } />,
	/* インライン要素のハイパーリンク表示用のコンポーネント その他のハイパーリンクも似たような記述で行けるはず */
	[INLINES.HYPERLINK]: node => <SomeHyperlinkComponent content={ node } />,
    },
    renderMark: {
        /* <b> 用のコンポーネント 下線とかイタリックも似たような記述で行けるはず */
	[MARKS.BOLD]: text => <SomeBoldComponent text={ text } />
	/* <code> 用のコンポーネント */
	[MARKS.CODE]: text => <SomeCodeComponent text={ text } />
    }
};

const PostTemplate = ({ pageContext }) => {
    const { post } = pageContext;
    const eyeCatch = getImage(post.eyeCatch);
    
    return (
        <>
          <GatsbyImage
            image={eyeCatch}
            alt={`${post.title} eyeCatch`}
          />
          <h1> {post.title} </h1>
	  <p>{post.createdAt}</p>
          {post.content && renderRichText(post.content, options)}
        </>
    )
};

export default PostTemplate;

ふへぇ、ちゅ、ちゅかれた…。まぁ、ざっとこんな感じです。ここに記述しているのは一部ですので、「は?俺の欲しい要素がねえぞ!」という方は、お持ちのゲバ棒を置いて参考記事の2つ目をご覧ください。renderNodeの一部では、nodeとchildrenの2つの引数をとりますが、基本的にはchildrenしか使わないかと。ただし、nodeに関しては省略できなかったのでご注意を。…ぶっちゃけ、引数の渡し方とかはちゃんと検証してません。途中で力尽きています。誰かかわってください

参考記事はこちら。
Using Rich Text with the Contentful Source plugin
rich-text-react-renderer #README.md

読者の皆さま:「…なんだ、全然ハマってないじゃん。」
投稿者:「なんだと!日本語の情報が全然なくて困ったんだぞ!プンプン」
読者の皆さま:「ポカーン」

↑要約:私よりできるプロの皆さま、どうか詳しい解説記事を書いてください。

…実際のところ、先程の設定は「ありゃ、ちょっと足が重いな」という程度のもので、たいしたぬかるみではありませんでした。しかしここから先はだいぶ抜け出すのがきつくなってまいります。

Rich Textにおける画像の表示

さて、当然ブログの記事には画像を設定したいですよね。そして、せっかくGatsbyを使っているならGatsby Plugin Imageを使って画像の遅延読み込みとかに対応したいのは当然のことです。…で、これもまた情報が見つからないということで、実際のコードをお見せしながら実装方法をご紹介します…。

/* gatsby-node.js */

exports.createPages = async ({ graphql, actions, reporter }) => {
 const { createPage } = actions;
 const result = await graphql(
   `
   query {
     allContentfulBlogPost(sort: {order: DESC, fields: createdAt}) {
       edges {
     node {
       finalUrl
       title
       createdAt(formatString: "YYYY/MM/DD")
       description
       eyeCatch {
         gatsbyImageData
       }
       content {
         raw
         references {
               ... on ContentfulAsset {
                 contentful_id
                 __typename
                 title
                 gatsbyImageData
               }
         }
       }
     }
       }
     }
   }
   `
 );
 
 if (result.errors) {
   reporter.panicOnBuild(`Error while running GraphQL query.`);
   return
 };
 
 const PostTemplate = path.resolve(`./src/templates/PostTemplate.tsx`);
 
 result.data.allContentfulBlogPost.edges.forEach(({ node }) => {
   createPage({
     path: `/services/${node.finalUrl}`,
     component: PostTemplate,
     context: {
       post: node,
     }
   });
 });

まずはgraphQLによるデータの取得から。Rich Textから画像データ等を取り出すためにはreferencesを経由します。その中にある、ContentfulAssetと言うのが画像データの本体ですね。一見するとgatsbyImageData以外は必要なさそうに見えますが、__typenameとcontentful_idがないと画像が表示されないという謎の問題があるらしい(以降に述べる問題にも関係ありそうですが、正直良くわからん)。詳しくはこちらのdiscussionをどうぞ。
 そして、表示部分はこんな感じ。

/* PostTemplate.tsx */

import React from 'react';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS } from '@contentful/rich-text-types';

const options = {
    renderNode: {
        /* 必要なコンポーネント以外は省略しています */
	/* 画像表示用のGatsbyImageコンポーネント */
        [BLOCKS.EMBEDDED_ASSET]: node => node => {
            return (
                <GatsbyImage
                    image={ getImage(node.data.target) }
                    alt={ node.data.target.title }
                />
            )
        },
    }
};

const PostTemplate = ({ pageContext }) => {
    const { post } = pageContext;
    const eyeCatch = getImage(post.eyeCatch);
    
    return (
        <>
          <GatsbyImage
            image={eyeCatch}
            alt={`${post.title} eyeCatch`}
          />
          <h1> {post.title} </h1>
	  <p>{post.createdAt}</p>
          {post.content && renderRichText(post.content, options)}
        </>
    )
};

export default PostTemplate;

先に触れた通り、Rich Text内部の画像表示はrenderRichTextのoptionsのBLOCKS.EMBEDDED_ASSETで指定します。Gatsby Image PluginにおけるgetImageの引数に渡すデータはnode.data.targetに入っているのでそれを渡しましょう。また、画像の名前はnode.data.target.titleでとってこれるのでこれをaltに入れれば解決。ちなみに、GtasbyImageコンポーネントにおいてはimageとaltの値が必須となっています。TypeScriptをご利用の方は頭の片隅においておいてください。いや〜、これで表示はオッケ〜!…とはなりませんでした。
 なんと、Gatsbyが猛り狂っています。
 Cannot query field "references" on type ...「referencesなんて存在しねぇーぞ!」
 …は?なんやて??ど、どうした、Gatsby、気が触れたか?
 はい、そんなわけありませんね。その怒りの理由はこちらのissueにありました。えぇ〜、超☆意訳します。「あ〜、せや、使うreferencesをいれたダミーデータがないとreferences読まれへんから。堪忍なぁ。」…まじ?
 そうなんです。残念ですが、投稿を始める前に、contentfulに利用するreferencesを登録したdummyのコンテンツがないと、指定したqueryが発行されないみたいなんですね。ただ、dummyコンテンツが実際に表示されてしまうのは困る…。そこでたどり着いたのがこんな感じの取得方法。

/* gatsby-node.js */

exports.createPages = async ({ graphql, actions, reporter }) => {
 const { createPage } = actions;
 const result = await graphql(
   `
   query {
     allContentfulBlogPost(filter: {url: {ne: "dummy"}}, sort: {order: DESC, fields: createdAt}) {
       edges {
     node {
       finalUrl
       title
       createdAt(formatString: "YYYY/MM/DD")
       description
       eyeCatch {
         gatsbyImageData
       }
       content {
         raw
         references {
               ... on ContentfulAsset {
                 contentful_id
                 __typename
                 title
                 gatsbyImageData
               }
   	... on allContentfulBlogPost {
                 contentful_id
                 __typename
                 finalUrl
                 title
                 description
                 eyeCatch {
                   gatsbyImageData
                 }
               }
         }
       }
     }
       }
     }
   }
   `
 );
 
 if (result.errors) {
   reporter.panicOnBuild(`Error while running GraphQL query.`);
   return
 };
 
 const PostTemplate = path.resolve(`./src/templates/PostTemplate.tsx`);
 
 result.data.allContentfulBlogPost.edges.forEach(({ node }) => {
   createPage({
     path: `/services/${node.finalUrl}`,
     component: PostTemplate,
     context: {
       post: node,
     }
   });
 });

見ての通り、queryの取得の際にfilterを使って、名称がdummyのモノ以外を取得するようにしています。…いや、おかしいだろ。もっと賢いやり方あるって。教えて!え◯い人!…とは言うものの、公式のissueでも「これ流石に微妙じゃない?」って意見は上がってたので、今後のアップデートに期待しましょう…。
 
投稿者:「ゼェゼェ、死ぬかと思ったぜ。まぁ、こんなぬかるみ、そうはないやろ!」

Rich Textにおける内部リンクの処理

―せや、あとは内部リンクとかも準備せんと!どれどれ、ほか記事へのリンクは…ほう!entryってのを使うんか!…どうやって??―こうして、筆者は僅かな情報をすすりながら、内部リンクの表示カードを制作するのでした…。
 ということで、こんな感じのコードに到達しました。まずはデータ取得から。

/* gatsby-node.js */

exports.createPages = async ({ graphql, actions, reporter }) => {
 const { createPage } = actions;
 const result = await graphql(
   `
   query {
     allContentfulBlogPost(filter: {url: {ne: "dummy"}}, sort: {order: DESC, fields: createdAt}) {
       edges {
     node {
       finalUrl
       title
       createdAt(formatString: "YYYY/MM/DD")
       description
       eyeCatch {
         gatsbyImageData
       }
       content {
         raw
         references {
               ... on ContentfulAsset {
                 contentful_id
                 __typename
                 title
                 gatsbyImageData
               }
   	... on allContentfulBlogPost {
                 contentful_id
                 __typename
                 finalUrl
                 title
                 description
                 eyeCatch {
                   gatsbyImageData
                 }
               }
         }
       }
     }
       }
     }
   }
   `
 );
 
 if (result.errors) {
   reporter.panicOnBuild(`Error while running GraphQL query.`);
   return
 };
 
 const PostTemplate = path.resolve(`./src/templates/PostTemplate.tsx`);
 
 result.data.allContentfulBlogPost.edges.forEach(({ node }) => {
   createPage({
     path: `/services/${node.finalUrl}`,
     component: PostTemplate,
     context: {
       post: node,
     }
   });
 });

今回の内部リンクのqueryはreferences内部の... on allContentfulBlogPostで扱われているようですね。内部データの子要素といった具合なんでしょうか。え?この情報をどこで手に入れたんだって?なぁに、簡単な話さ。dummyデータに入れた内部リンクの情報を自分で確認したんですよ(遠い目)。いや、簡単になってるのはわかりますよ。もうひと超え!もうほんのちょっとだけ簡単にしてくれるとありがたいなぁって…そうですか、だめですか。
 また、こちらに関しても画像同様、内部リンクをdummyデータに入れないと、Gatsbyがキレます。気を付けましょう。
 表示方法に関しては画像と変わりません。

/* PostTemplate.tsx */

import React from 'react';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS } from '@contentful/rich-text-types';

const options = {
    renderNode: {
        /* 必要なコンポーネント以外は省略しています */
	/* 内部リンク表示用のコンポーネント */
        [BLOCKS.EMBEDDED_ENTRY]: node => {
            return (
                <EntryComponent
                    title={node.data.target.title}
                    image={node.data.target.eyeCatch}
                    finalUrl={node.data.target.finalUrl}
                />
            )
        },
    }
};

const PostTemplate = ({ pageContext }) => {
    const { post } = pageContext;
    const eyeCatch = getImage(post.eyeCatch);
    
    return (
        <>
          <GatsbyImage
            image={eyeCatch}
            alt={`${post.title} eyeCatch`}
          />
          <h1> {post.title} </h1>
	  <p>{post.createdAt}</p>
          {post.content && renderRichText(post.content, options)}
        </>
    )
};

export default PostTemplate;
/* EntryComponent.tsx */

import React from 'react';
import { navigate } from "@reach/router"
import { GatsbyImage, getImage } from 'gatsby-plugin-image';

const EntryComponent = ({title, image, url}) => {
    const eyeCatch = getImage(image);
    const handleOnClick = () => navigate(`/posts/${ finalUrl }`)
    
    return (
        <div onClick={handleOnClick}>
	    <p>{ title }</p>
	    <GatsbyImage
                image={ eyeCatch }
                alt={`${title} eye catch`}
                className={classes.cardMediaContent}
            />
	</div>
    )
};

export default EntryComponent;

スタイルに関してはご趣味に合わせて調整してもらう感じで。表示に際して注意するべきはURLの発行でしょうか。今回はposts下に各postを置く形を想定していますが、別の管理方法をとっている方はここでのURLの発行の際に一工夫する必要があると思います。…というか、本当に情報少なくないっすか??

投稿者:「ハァハァハァ、なんとかなったか。大漁だしそろそろ帰るか!…あれ、あ、足が…」

Rich TextとCode Blockと猿

開発記事系の記事を書く以上、ソースコードをかくためのコードブロックは欲しいですよね。結論から申し上げます。ContentfulのRich Textにコードブロックはありません。あれ、幻聴?コードブロックがないなんてそんな…。

おきのどくですが
ひっしゃのいしきは
きえてしまいました

…ハッ! まだだ!自分で!自分で作るんだ!ウキィー!!(錯乱)
 そして生成されたおどろおどろしい得体の知れない何かがこちら。

/* PostTemplate.tsx */

import React from 'react';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS } from '@contentful/rich-text-types';

const options = {
    renderNode: {
        /* 必要なコンポーネント以外は省略しています */
	/* 段落用のコンポーネント…だった。 */
        [BLOCKS.PARAGRAPH]: ( node, children ) => {
            if ( String(children[0]).includes('<Code Block>')) {
                return (
                    <CustomCodeBlock content={children} />
                )
            } else {
                return (
		    /* 本来の段落用コンポーネント */
                    <SomePComponent content={ children } />
                )
            };
        },
    }
};

const PostTemplate = ({ pageContext }) => {
    const { post } = pageContext;
    const eyeCatch = getImage(post.eyeCatch);
    
    return (
        <>
          <GatsbyImage
            image={eyeCatch}
            alt={`${post.title} eyeCatch`}
          />
          <h1> {post.title} </h1>
	  <p>{post.createdAt}</p>
          {post.content && renderRichText(post.content), options}
        </>
    )
};

export default PostTemplate;
/* CustomCodeBlock.tsx */

import React from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { atelierCaveDark } from "react-syntax-highlighter/dist/esm/styles/hljs";

const codeContentformat = codeContent => {
    const languageAssign = codeContent.match(/<language:\s\w+>/)[0];
    const language = languageAssign.split('')[1].replace('>', '');
    const codeContentAdjusted = codeContent.replace('<Code Block>', '').replace(/<language:\s\w+>/, '').replace(/\r?\n/, '');

    return [language, codeContentAdjusted]
};

const CustomCodeBlock = ({ content }) => {
    const [language, codeContentAdjusted] = codeContentformat(content[0])

    return (
        <SyntaxHighlighter
            showLineNumbers
            style={atelierCaveDark}
            language={language}
        >
            {codeContentAdjusted}
        </SyntaxHighlighter>
    )
};

export default CustomCodeBlock;

…は?全ての人類は思った、こいつは猿だと。
 いや、本当にすいません、弁解させてください。本来用意されている要素であれば、指定の形式で抜き出し、スタイルを与えることができるんですよ。でも今回の場合それがなくてですね、私程度の力では処理に割り込むことも能わず…。と、とりあえずコードの説明をさせてください。
 まずデータの取得ですが、指定がない場合、ブロックは必ず段落として配列形式で渡されることを利用し、特定の文字列が含まれるものをコードブロックとして認識します。ちなみに、children[0]Stringであることを明示的にしないと「文字列じゃないからincludesなんて使えないよ」って怒られるので、わからせます。
 次に表示部分。ここではReact Syntax Highlighterっていうイカしたコードブロック表示を実現するライブラリを使います。なんと、Styleと言語指定、ちょっとしたオプションを追加するだけでいい感じの表記をしてくれるんですね。まぁ、この辺は情報が多かったので他の方に任せましょう。問題はこの謎の関数codeContentformat。こいつはこんなことをしてます。

  1. 指定した方法で書かれた言語指定の箇所を取り出す。
  2. 文字列をバラして言語名だけを取り出す。
  3. CodeBlockであることを示す部分、言語を指定する部分、それが記述された行の改行をそれぞれ削除。

これによって、晴れてコードブロックを制作、表示させることができました!
 …うん、やっぱりこれはキツい。いつ見ても、どこから見ても猿だ、動物園だ。さっきまで干潟にいたはずなのに…。何よりこのコード、なんか遅いと思う。現に開発系のページだけ表示速度遅いし…。できれば改善案をいただきたい限りですので、何卒ご教授いただけますと幸いです。
 …それもこれも、ContentfulがCode Blockの表記に対応してくれればいいんです!本当に救いの手が欲しい、今日この頃でございます。

投稿者:「な、何はともあれ機能面はなんとか…。あとは記事を書いていけば グハァ」
実況:「お〜っと!ここにきて、まさかの、死体蹴りダァーー!」

Rich Textと日本語入力は二人三脚ができない

ようやっとブログ記事を描き始めた私。しかし…何かがおかしい。Contentfulで入力していると、カーソルが単語の頭に戻ってしまう。それも半端に変換がかかった上で…。どうなってるんだ?拡張機能を切ってみてもだめ。ブラウザを変えてもPCを変えてもOSを変えてもだめ。明らかにこちらの問題とは思えない謎の動作、情報も全くない…。結局、公式に問い合わせました。…あのぉ、もうちょっとできる方が改善に当たってくれたりは…ヒィ! スンマセン! して、その公式の回答はこんな感じ。

「あ〜、それ多分こちら側のミスだわ。ローマ字→ひらがな→漢字って変換してる途中に、Contentfulの自動保存機能が動いて気持ち悪い保存のされ方してるんだと思う。数ヶ月内にその辺を改善するアップデート施すから、そこのロードマップに追加しとくよ。待っててね〜(注:意訳です)」

と言うわけでね。しばらくContentfulで日本語で記事を書くのは厳しそうです。一応、Google Documentからコピペするとうまく行くので、しばらくはそれを使って書いていきたいと思います…。

投稿者:「…………ガクッ」

まとめ

「頼むから、みんなでもっと使って!干潟の潮干狩り勢にこんなことさせないで!」 正直、今まで使った組み合わせのどれよりも情報が少なかったと思います。確かに、この手のサイトを作るならwordpressが簡単で早いでしょう。ただ…もう少し…その…この辺の技術も触ってみませんか?いや、おそらく、使ってらっしゃる方はたくさんいるんでしょうけど…その手の皆さんは「できて当たり前」って感じで…。そうなると、基本的な情報がなかなか出てこないじゃないですか。ほんとはそう言う情報があったほうがもっと普及すると思うんですよ。私みたいな浅瀬の民が使うので。
 つまり、 「一緒に潮干狩り行く人募集」 ってことです。おしまい。

追記

前回が1万字、今回が2万字というとんでもない分量になってしまった…。どうしたらもっと簡潔に記述できるのでしょうか…。

Discussion

自分も似たような構成使っていますが、ContentfulのRichTextは全然リッチじゃないと言わざるを得ません
コードブロック以外にも

  • 文字の色を変えられない(例えば対談記事で話者名にそれぞれ色を付けたいときなど)
  • 表が組めない
    あたりが個人的には致命的です

エンジニアが自分で使う分にはRichText辞めてMarkdownにするのが一番いいのかなあと
非エンジニアが記事投稿する場合Extensionでもっと機能充実したWYSIWYGのテキストエディターを用意するとかですかねえ...

コメントありがとうございます。

他にもそんな問題があったとは…知りませんでした。
まぁ、悲しむには値しますが驚くには値しませんね、本当に残念ですが。

私は非エンジニアなので(必死の抵抗)、なんとかRichTextを使いたいと思っているんですが、結局こちらにもMarkdownで投稿してますし、結局MarkDownが便利に感じちゃってますね…。

…やっぱり構成変更したほうがいいですか…。

大変参考になる記事、ありがとうございました。
「Rich Textにおける画像の表示」において、

{post.content && renderRichText(post.content), options}

とされているようですが、post.contentとRenderRichText()の論理積を取られている理由を教えていただければ幸いです。また、optionsは、renderRichText(post.content, options)とするのが正しいと思います。

コメントありがとうございます。

post.contentとRenderRichText()の論理積を取られている理由を教えていただければ幸いです。

個々なんですが、正直なくても大丈夫だと思います。
renderRichTextは引数が空だと描画段階でエラーを吐いちゃうのでその対策ってわけですが、そもそも記事を更新するときにここが空になるケースはだいぶ考えにくいのではないでしょうか。

optionsは、renderRichText(post.content, options)とするのが正しいと思います。

ご指摘ありがとうございます。
その通りですね。ほかの個所でも同様のミスがありましたので、合わせて修正させていただきました。

大変参考になる記事でした。

Cannot query field "references" on type ...:「referencesなんて存在しねぇーぞ!」

この部分でうまく挙動せず私も躓いてしまっているのですが、

投稿を始める前に、contentfulに利用するreferencesを登録したdummyのコンテンツがないと、指定したqueryが発行されない

というのは具体的にどのようにダミーコンテンツを作成したのでしょうか。

コメントありがとうございます。

具体的にどのようにダミーコンテンツを作成したのでしょうか。

なんてことはありません、contentfulですべての要素を含んだ記事を書くだけです。

「referencesが無いぜよ」と言われてしまう問題は、「内部リンク」「画像」の2つで発生します(少なくとも私の眼の前に出現したのはコイツラでした)。
投稿した記事全体の中で、上記2つの要素が無いと判断されるとreferences事態が発行されないんでしょう。
ということで、dummyというタイトルの記事を作成し、内部リンクから画像から(一応、コードブロックや箇条書きも)今後利用しうるすべての要素を使って適当な文字列を打ち込んで公開しました。
ただ、そんなメチャクチャなダミー記事を掲載されると困ってしまうので、タイトルが(私はurlという要素を用意していたのでその部分が)dummyであるものを拾い、掲載対象から外したという具合です。

…私は一応これでなんとかなりましたが…。
少しでもお役に立てば(あわよくば私のような浅瀬の民を助けてくれれば)幸いです。

ログインするとコメントできます