あらまし
結構長くはてなブログにお世話になっていたが、
- 適当に運営していたのでタグやカテゴリ管理ができていない
-
scrapbox.ioのUI, UXに魅力を感じ、
パク…インスパイアされた - 自身で開発したい気分になった
などの理由でブログを作ろうと思い立った。
なお、完成品はこちら
投稿画面
欲しかった仕様
- scrapboxに影響を受けたハードルの低い投稿画面
- 入力欄は本文のみで、一行目をタイトルと認識、#に続く文字をハッシュタグと認識
- その他はMarkdownに準ずる
- ハッシュタグによる検索機能
- タグで検索し、引っかかった記事がズラッと並ぶ
- ハッシュタグ自動リンク機能
欲しかった仕様から逆算した仕様
- サーバーサイドの機能はContentfulにお任せ
- Headless CMSと呼ばれる、CMSから見た目部分を取っ払いデータ部分に特化した仕様
- 最初はFirebaseでやろうとしたけど、DB設計が大変そうなので撤退
- フロント側はNuxt.jsを採用
- VueJSのコンポーネントで組み立てるフレームワーク
- 単純に使い慣れていたので
-
herokuにデプロイ
- 単純に使い慣れていたので
Contentful+Nuxtで小手調べ
まずは存在する記事を全表示するトップページを作ろうと思ったが、
公式Docの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}}
でタイトルを表示
- script部で
注意点
そんなに難しくないが、titleがpostオブジェクト直下ではなく、post.fields.titleに入っているのでちょい迷った。
他にもbody(本文)やtagsなど、Contentfulで自分で設定したフィールドはfieldsに入る。
idやcreateAtなど、Contentful側が自動で作るフィールドはsysに入るようだ。
今回の仕様に即したフィールド構成でもう一回
blogのテンプレートにはblogPostとuserの2つのContent Modelがあり、
blogPostの中にはheroImageなども入っていたのだが、今回のブログ仕様では使わないので新たなContent Modelを作った。
下のように、簡単に作れる。
Dateはいるかなと思って作ったがcreateAtを利用することにしたので削除した。
で、nuxtの方からも新しいContent Modelを呼び出すように変更。
その際、
~/
~/{tag}
~/{tag}/{page}
など複数のURLからAPIを呼ぶことになるので、リスト表示用componentを作成した。
あとこの辺で見た目部分をBootstrap Vueにした。
注意点
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を導入し(過去に使用したことがあったため)頑張って叩いた。
おそらくこんな感じになろうという例がこれ
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にも説明があるが、SSRだとその時点ではlocationは存在しないので、undefinedになってしまう。
結局、nuxtのページから
<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
あたり。
みなさんもお気をつけて〜