gasekao.com
2020-11-05

nuxt.js×prismic でのブログ構築とヘッドレスCMSについて

nuxt.jsとprismic.ioで超簡易的なブログを作りました。

ブログ構築となればwordpressを利用してもいいんですが、ただただprismic.ioを利用した構築がしたいという一心、その目的達成の為だけに構築しました。

構成

言語:Javascript(pug、sass)
フレームワーク:Nuxt.js
CMS:prismic.io
ホスティング:heroku(SSL対応、PointDNS利用)
その他:GoogleAnalytics、Search Console、@nuxtjs/sitemap

prismic.io

prismic.io は サンフランシスコのPrismic社のヘッドレスCMSサービスです。
ヘッドレスCMSということで、サービスから用意されているのは管理画面のAPIのみ。
その他の開発言語やフレームワーク等の技術選定は開発者に委ねられています。
最近はフロントエンド技術が盛んでPWAやSSRでの構築も主流ですし、技術者人口の増加や開発手段の平易化から考えてもこのようなサービスが台頭するのは至極当然のことですね。
日本製の同じようなCMSではmicroCMSがありますが、prismic.io よりは(開発者感覚でいえば)随分後にサービスインしていたと思います。ようはフォロワー。

microCMSは利用したことがないんですが、ざっとドキュメントを確認した限りでは簡単に導入ができそうでした。
大きな違いで言うと、microCMSは「本文」として入稿されるコンテンツ部分がhtmlベースで保存されているという部分ではないでしょうか。

サービス側での定義の問題だとは思いますが

Headless CMS = viewを持たない(フロントエンドを持たない)

と考えたときに、「htmlの要素を含むと、Headless ではなくなるのではないか?」というのが私の感想です。ヘッドレスであれば、デザインやレイアウトといったページの構造やDOMはサーバーサイドが関与するべきではなく、フロントに委ねるのが正でしょう。HTMLで書かれているということ自体が構成要素・情報だと考えると、元来の「Hyper text mark language」という書式で書かれた書類という意味合いを無視しかねないので、如何とも言い難いです。ここまでいくと、おそらく杞憂ですね。

決してmicroCMSを批判したいわけではなく、フロントエンドでのデザインやレイアウトの自由度を考慮したときに
より私の理想に近い Headless CMS は prismic.io でした。

その代わり、構築コストは格段に高いと思います。
APIから取得できるリッチテキストで書かれたコンテンツ部分のJSONは以下の配列の形式なので、DOM化するのは一手間かかります。

[
             {
                "type": "paragraph",
                "text": "段落の内容、段落の内容、段落の内容",
                "spans": [
                    {
                        "start": 3,
                        "end": 21,
                        "type": "strong"
                    },
                    {
                        "start": 28,
                        "end": 56,
                        "type": "strong"
                    }
                ]
            },
            {
                "type": "heading2",
                "text": "見出し2の内容",
                "spans": [
                    {
                        "start": 0,
                        "end": 24,
                        "type": "strong"
                    }
                ]
            },
            {
                "type": "paragraph",
                "text": "段落の内容2、段落の内容2、段落の内容2、段落の内容2",
                "spans": []
            }
]

無論リッチテキストをマークアップさせる関数は用意されており、HTML Serialiser を噛ませることで関数のカスタマイズも可能ですが、非開発者にとってはハードルの上がる内容ではないでしょうか。(その証拠(?)に非開発者向けのページが用意されています。)

あとは、prismic.io には日本語の公式ドキュメントはないです。個人的に導入方法をわかりやすく記事にしている方がいらっしゃったので私も参考にしており、以下で私の導入手順は記載しますが。

導入手順ダイジェスト

nuxt アプリケーションの構築は省略。

$ npm install -dev @nuxtjs/prismic prismic-dom

nuxt.config.json に以下を追加

  // Modules (https://go.nuxtjs.dev/config-modules)
  modules: [
    //...その他利用するモジュール...
    '@nuxtjs/prismic'
  ],
  prismic: {
    endpoint: 'https://gasekao.cdn.prismic.io/api/v2',
    apiOptions: {
      access_token: "自分のprismic API アクセストークン"
    },
  },

一覧を取得したいページの asyncData

async asyncData({ $prismic, error }) {
    try{
        const list = (
          await $prismic.api.query(
            $prismic.predicates.at('document.type', 'prismicに設定したコンテンツタイプ名'),
            { orderings : '[my.blog_post.date desc]' } //日付降順に取得
          ))
        if (list_) {
          return { list }
        } else {
        error({ statusCode: 404, message: 'Page not found' })
        }
    } catch (e) {
        error({ statusCode: 404, message: 'Page not found' })
    }
  },

$prismic.api.query の関数で一覧を取得すると、取得件数や取得順クエリを設定することができ、A more complex example のようにページネーションやソート機能をつける際はこの関数を利用することになります。

コンテンツを個別に表示したいページのasyncData

async asyncData({ $prismic, error }) {
    try{
        const document = (await $prismic.api.getByUID('prismicに設定したコンテンツタイプ名', '記事のUID'))
        if (document) {
            return { document }
        } else {
            error({ statusCode: 404, message: 'Page not found' })
        }
    } catch (e) {
        error({ statusCode: 404, message: 'Page not found' })
    }
 },

任意に設定したコンテンツのUIDで呼び出すAPIです。「Type an SEO-friendly identifier...」と推奨されているので、コンテンツの内容の意味を持つ文字列が好ましいですね。私はこの構成をフルに生かすために、ドメイン直下に記事を設置することで記事の全てが第1階層のコンテンツとするのが好きです。

テンプレート部分では、当ブログではこのようになっています

.post_document_container
        .post_document_catch_bg( :style="{backgroundImage: 'url(' + document.data.eyecatch.url + ')'}")
            .post_document_catch( :style="{backgroundImage: 'url(' + document.data.eyecatch.url + ')'}" )
        .post_document_inner
            .post_document_date {{document.data.post_date}}
            h1.post_document_title {{document.data.title[0].text}}
            ul.post_tag_list
                li.post_tag_item(v-for=" (tag, index) in document.tags ")
                    a() {{tag}}
            .post_document
                prismic-rich-text( :field="document.data.content" :htmlSerializer="htmlSerializer")

pug で書かれている + class名が訳わからないことにはなっていますが、

<prismic-rich-text>のコンポーネントに、field でリッチテキストの変数を渡し、HTML Serializer を利用する場合も変数を渡します。

HTML Serializer 利用時は、plugins ディレクトリに html-serializer.jsを作成し以下を設定

import linkResolver from "./link-resolver"
import prismicDOM from 'prismic-dom'
const Elements = prismicDOM.RichText.Elements

export default function (type, element, content, children) {
  if (type === Elements.hyperlink) {
    let result = ''
    const url = prismicDOM.Link.url(element.data, linkResolver)
    if (element.data.link_type === 'Document') {
      // Note: https://github.com/nuxt-community/prismic-module/issues/60
      result = `${content}`
    } else {
      const target = element.data.target ? `target="'${element.data.target}'" rel="noopener"` : ''
      result = `${content}`
    }
    return result
  }

  if (type === Elements.image) {
    let result = `${element.alt || ''}`
    if (element.linkTo) {
      const url = prismicDOM.Link.url(element.linkTo, linkResolver)
      if (element.linkTo.link_type === 'Document') {
        // Note: https://github.com/nuxt-community/prismic-module/issues/60
        result = `${content}`
      } else {
        const target = element.linkTo.target ? `target="${element.linkTo.target}" rel="noopener"` : ''
        result = `${result}`
      }
    }
    const wrapperClassList = [element.label || '', 'block-img']
    result = `

${result}

` return result } if (type === Elements.preformatted) { let result = `
${element.text}
` return result } return null }

同じく、plugins ディレクトリに link-resolver.js を作成して以下を記入。

export default function (doc) {
    if (doc.type === 'blog') {
      return '/' + doc.uid
    }
    return '/'
  }

先述の nuxt.config.js を、以下に変更。

  prismic: {
    endpoint: 'https://gasekao.cdn.prismic.io/api/v2',
    apiOptions: {
      access_token: '自分のprismic API アクセストークン',
      linkResolver: "@/plugins/link-resolver",
      htmlSerializer: "@/plugins/html-serializer"
    },
  },

他にも私は、prismic記事による動的ページにsitemap利用したり、vue-highlight.js を利用したりしているのでチョコチョコ設定はありますが、だいたいこんなもんで導入は可能です。暇があればタグ検索はつけるつもりですし、記事がたくさん増えることがあればページネーションも実装する予定です。その時には再度導入手順を記事にすると思います。

おわりに

prismic導入という目標は達成しましたし、仕事でヘッドレスCMSを量産している身として設計の理解も進めることができたので、これ以上掘り下げたりサービスとして何か構築することはないと思います。

私は可愛い名前の技術やサービスを触るのが好きですし、次の本業?の案件はRubyエンジニアを名乗るために初めて Rubyで構築しようかと思っているので Hanami を触る予定です。

BACK TO TOP