Contentful+Nuxtでブログ作成 オリジナル記事投稿画面に苦労

# あらまし

結構長く[はてなブログにお世話になっていた](http://mi-ki-ri.hatenablog.jp/)が、

- 適当に運営していたのでタグやカテゴリ管理ができていない
- [scrapbox.io](https://scrapbox.io)のUI, UXに魅力を感じ、~~パク~~…インスパイアされた
- 自身で開発したい気分になった

などの理由でブログを作ろうと思い立った。

なお、完成品はこちら

<img width="1429" alt="スクリーンショット 2019-03-09 19.20.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/92027/6a777938-fa56-21cf-6f07-7338adfa033d.png">

投稿画面

<img width="1154" alt="スクリーンショット 2019-03-09 19.32.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/92027/3467b1ca-d20d-98e7-7f9e-ca2d96062e64.png">


[HashMikiri](https://hash.mikiri.net/)



# 欲しかった仕様

- scrapboxに影響を受けたハードルの低い**投稿画面**
    - 入力欄は本文のみで、一行目をタイトルと認識、#に続く文字をハッシュタグと認識
    - その他はMarkdownに準ずる
- ハッシュタグによる検索機能
    - タグで検索し、引っかかった記事がズラッと並ぶ
- ハッシュタグ自動リンク機能

# 欲しかった仕様から逆算した仕様

- サーバーサイドの機能は[Contentful](https://www.contentful.com/)にお任せ
    - Headless CMSと呼ばれる、CMSから見た目部分を取っ払いデータ部分に特化した仕様
    - 最初はFirebaseでやろうとしたけど、DB設計が大変そうなので撤退
- フロント側は[Nuxt.js](https://ja.nuxtjs.org/)を採用
    - VueJSのコンポーネントで組み立てるフレームワーク
    - 単純に使い慣れていたので
- [heroku](https://heroku.com/)にデプロイ
    - 単純に使い慣れていたので

# Contentful+Nuxtで小手調べ

まずは存在する記事を全表示するトップページを作ろうと思ったが、

[公式DocのNuxtの部](https://www.contentful.com/developers/docs/javascript/tutorials/integrate-contentful-with-vue-and-nuxt/)を見ればそこはなんとかなった。

なお、自分の環境だと `npm i -g contentful-cli` がエラーで入らなかったので、

Spaceの作成はContentfulのWebサイトを使って行った。

## ざっと手順

- nuxtのテンプレートを展開
- Contentfulに接続するjsライブラリをインストール(`npm install --save contentful`)
- `./plugins/contentful.js`に公式Doc通りの記述をすることでnpmのcontentful.jsをラップ。たぶんセキュリティ的にこうしている
- `.contentful.json`にトークンとか書く(トークンは本来cliで取れるようだが、自分はWebから入手)
- `./nuxt.config.js`で今のjsonを読むようにする。たぶんセキュリティ
- nuxtのpagesディレクトリで、
    - script部で`asyncData ({env})`内で`client.getEntries`することでpostsが返ってくる（公式のDocではPersonも取得している）
    - postsをテンプレート部で`v-for="post in posts"`してpostごとにアクセス可能に
    - `{{post.fields.title}}`でタイトルを表示

## 注意点

そんなに難しくないが、titleがpostオブジェクト直下ではなく、post.fields.titleに入っているのでちょい迷った。

他にもbody(本文)やtagsなど、Contentfulで自分で設定したフィールドはfieldsに入る。

idやcreateAtなど、Contentful側が自動で作るフィールドはsysに入るようだ。

# 今回の仕様に即したフィールド構成でもう一回

blogのテンプレートにはblogPostとuserの2つのContent Modelがあり、

blogPostの中にはheroImageなども入っていたのだが、今回のブログ仕様では使わないので新たなContent Modelを作った。

下のように、簡単に作れる。

<img width="833" alt="スクリーンショット 2019-03-09 18.16.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/92027/f44bbc5f-e9c8-7635-140c-6aa2ebfdf4a4.png">

Dateはいるかなと思って作ったがcreateAtを利用することにしたので削除した。

で、nuxtの方からも新しいContent Modelを呼び出すように変更。

その際、

- `~/`
- `~/{tag}`
- `~/{tag}/{page}`

など複数のURLからAPIを呼ぶことになるので、リスト表示用componentを作成した。

あとこの辺で見た目部分を[Bootstrap Vue](https://bootstrap-vue.js.org/)にした。

## 注意点

componentでは`asyncData ({env})`は使えなかった…

本来componentにtagとpageを渡すとAPIを叩いてくれるというふうにしたかったところ、

pageのほうでAPIを叩いてpostsを取得し、postsをcomponentに投げるという方式になったが、

結局同じような処理(API叩く)を3通り書いてしまったので反省点。

# いよいよ投稿画面

ここまでは**壮大な前フリ**で、ここから投稿画面制作に入る。

まずいきなりつまずいたのは、APIはAPIでも**コンテンツ取得用とマネージ用ではものが違う**ということ。

`npm i contentful`で入るやつは**Content Delivery API**しか触れないらしい。

つまりブログポストしたかったら、Ajaxで直にAPIを触るしかないのだ。

axiosを導入し(過去に使用したことがあったため)頑張って叩いた。

おそらくこんな感じになろうという例がこれ

``` js
postToContentful(){

        axios.post( `https://api.contentful.com/spaces/${space}/environments/${env}/entries` , {
          fields: {
            title:{"en-US": "タイトル"},
            body: {"en-US": "コンテンツのボディ"},
            tags: {"en-US": ["タグ", "タグ2"]},
            
          }
        }, {
          headers: {
            "Authorization": `Bearer ${token}`,
            "Content-Type": 'application/vnd.contentful.management.v1+json',
            "X-Contentful-Content-Type": `${contentType}`
          }
        })
          .then(response => {
            console.log(response);
          }).catch(error => {
            console.log(error);
          })
      }
```

この`postToContentful()`を呼び出すことでテスト記事が投稿される…はず！

## 注意点

- ローカライズのためだと思うが、タイトルなどは"en-US"の中に書かねばならない。USじゃないけどJaにする方法が分からないので諦めた
- タグなどリストのフィールドは{`[{"en-US": "タグ1"},{"en-US": "タグ1"}]`ではなく、{"en-US": ["タグ1","タグ2"]}
- URLのenvをWebから探すのに手こずったので、APIを叩いて確認した。初期設定では"master"だと思う

# Oauth

この時点ではパスワードを**ハードコーディング**することで投稿制限を実現していたのだが、

やはりセキュリティ的に不安なのでOauthを導入してみた。

注意点:contentfulのOauth Appは「ユーザー」画面から作る。行き方が分からなくて迷った。

所定のページにアクセスすると`http://localhost:8080/my-app/#access_token=$CONTENT_MANAGEMENT_API_ACCESS_TOKEN`のような形でアクセストークンが得られる。

これで楽勝だぜ！　と思ったら思わぬところでつまずいた。

`location.hash`でURLのハッシュ部分を取ろうと思ったら、locationが**undefined**。

[公式Docにも説明がある](https://ja.nuxtjs.org/faq/window-document-undefined/)が、SSRだとその時点ではlocationは存在しないので、undefinedになってしまう。

結局、nuxtのページから

```XML
    <no-ssr>
      <MyForm />
    </no-ssr>
```

のようにMyFormを呼び出す(その際no-ssrでくくる)ことで事態を打開できた。

# ひとまず完成

デザインとかRSSとかちまちまやることはあったのだが、ひとまず基本部分は完成した。

Herokuへデプロイしてhobbyアカウントにアップグレード、SSL通信もできるようになったよ。

自分がつまずいたのは

- `post.title`ではなく`post.fields.title`
- componentでは`asyncData ({env})`は使えない
- ContentfulのAPIには2種類ある
- en-US
- Oauth Appはユーザー画面から作る
- location が undefined

あたり。

みなさんもお気をつけて〜