WordPressからNuxt+Firebaseへの移行 ② プロジェクト立ち上げ、記事作成画面、マークダウンエディター
2019.11追記
ここに書いてあるのは古い内容で、あまり筋が良くなかったので現在は作業方針を変えています。詳しくは以下の記事をご覧ください。
新・WordPressブログをNuxt+Firebaseに完全移行するプロジェクト
半年ほど前に、こんな記事を書きました。 [nlink id="5837"] 簡単にまとめると、 ・WordPressの重... 続きを読む
Contents
前回までのあらすじ
脱WordPressしてHeadless CMSに移行することを決めたものの、記事数が多すぎてContentfulを使えないため、データ保存にFirestoreを使うことを決意しました。
この移行作業中も現行のブログを更新していくつもりなので、
データ保存以外の作業を全部終わらせてから最後にデータ引っ越しする方が効率が良いなと思い、まずはFirebaseで新規ブログを作る気持ちで開発を始めることにしました。
※この記事はVue.js / Nuxt.jsについてはある程度わかっている前提で書いています。初めてNuxt.jsに触れるという方がこれをやるのはオススメしません。
Nuxt.jsのプロジェクトを準備
さすがにVS Codeとかyarnとかの説明は省略。
インストール – Nuxt.jsに従ってcreate-nuxt-appします。
とりあえずLinter、Prettier、Axiosなどは全部入れておきます。
この画面でついでにsass-loaderとかも入れられれば別途入れるものがなくなるのだけど、ないのでyarn add --dev node-sass sass-loader
。
あとはnuxt.js(v2)の作業ディレクトリを整理 – Qiitaで作業フォルダをsrcに移す程度。
コードの自動フォーマット
ESLintとPriettier、別に邪魔になるものでもないのでとりあえず設定しておいて損はないかと思います。
ESLintの設定は開発ツール – Nuxt.jsで。
基本的にはnuxt.config.jsも.eslintrc.jsも全てコピペすれば良いと思うのですが、nuxt.config.jsのextendの中身を
// Run ESLint on save
if (ctx.isDev && ctx.isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/,
options: {
fix: true
}
})
}`
と、optionsでfix: trueを指定すると、ファイルを保存するたびに勝手にコードの整形まで走らせてくれます。便利な時代になりました。
.eslintrc.js
に関しては、余計なカスタムをせずに広く支持されているルールに従うべきだと思っていますが、
// add your custom rules here
globals: {
alert: false,
document: false,
console: false,
location: false,
process: false,
firebase: false
},
rules: {
"semi": [2, "never"],
"no-console": "off",
"vue/max-attributes-per-line": "off",
"prettier/prettier": ["error", { "semi": false }]
}
最低限、グローバル変数の定義は必要と思います。
Bulmaとmaterial-iconsを追加
CSSフレームワークとしてBulmaを導入。
WordPressでは少しでも読み込みを軽くするためにmoduleに分けて読み込んでいましたが、Nuxt化するならそんなことに気を遣いたくないので丸ごと。
普通に使うだけならyarn add @nuxtjs/bulma
でも良いのですが、
nuxt.jsに導入したbulma.ioを独自の色でカスタマイズする。 – Qiita
こちらを参考にして変数を読み込みました。
$primary: #898989;
$footer-padding: 1rem;
@import "~bulma/bulma";
※以降、classは全てbulmaを前提に書いていきます。デザイン面のクラスは適宜修正してください。
他にマテリアルアイコンも使いたいので、
{ href: 'https://fonts.googleapis.com/icon?family=Material+Icons', rel: 'stylesheet' }
をheadのlinkに追記します。favicon.ico行の下です。
記事の新規投稿画面を作成
表示ページはFirebaseにデータを送ってから考えるとして、まずは記事投稿画面から。
pagesディレクトリ内に管理画面っぽいものを作っていきます。loginとかpost/indexとかも必要ですが、とりあえず。
枠組みを作成
まずはcreate.vue
の中身を作っていきます。当然のbulma。
<template>
<article class="box">
<h3 class="title is-size-3">
新規投稿を追加
</h3>
<div class="form">
<div class="field">
<label class="label"></label>
<input type="text" class="input" />
</div>
<div class="field">
<label class="label"></label>
<select class="select">
<option disabled selected></option>
<option></option>
<option></option>
</select>
</div>
<div class="field">
<label class="label"></label>
<input type="text" class="input" />
</div>
<div class="field">
<label class="label"></label>
<textarea class="textarea">textarea>
</div>
</div>
</article>
</template>
<style lang="scss" scoped>
.textarea {
min-height: 10em !important;
}
</style>
ハリボテのそれっぽい画面ができましたが、カテゴリーは既にある配列から選ばせたいし、タグも配列で送信する必要があります。
タイトル・カテゴリー・本文設定の実装
とりあえずタグは処理が面倒そうなので後回しにしてそれ以外を先に。
カテゴリーとタグはFirebaseから取ってくる必要がありますが、今はまだデータがないので、一旦ローカルで仮のデータを作ります。
<div class="form">
<div class="field">
<label class="label">タイトル</label>
<input v-model="title" type="text" class="input" />
</div>
<div class="field">
<label class="label">カテゴリー</label>
<select v-model="categories" class="select">
<option disabled selected>カテゴリーを選択</option>
<option
v-for="category in options.categories"
:key="category.id"
:value="category"
>{{ category.name }}</option
>
</select>
</div>
<div class="field">
<label class="label">タグ</label>
<multiselect
v-model="tags"
tag-placeholder="Add this as new tag"
placeholder="Search or add a tag"
label="name"
track-by="name"
:code="options.tags.id"
:options="options.tags"
:multiple="true"
:taggable="true"
@tag="addTag"
></multiselect>
</div>
<div class="field">
<label class="label">本文</label>
<textarea v-model="content" class="textarea"></textarea>
</div>
<div class="button is-link">
送信
</div>
</div>
export default {
components: {
Multiselect
},
data() {
return {
title: '',
categories: '',
tags: [],
content: '',
options: {
categories: [
{
id: 4,
name: 'ひとりごと'
},
{
id: 109,
name: '最近の話題'
}
]
}
}
},
computed: {
post() {
return {
title: this.title,
categories: this.categories.id,
content: this.content
}
}
}
}
`
①optionsをFIrebaseから取得(今はローカル)
②v-modelとdataを連動
③computedのpostに送信する情報をまとめる
という流れです。
Vue-Multiselectでタグ設定メニューの実装
残るはタグ。
どうせなら入力補完も欲しいですが、自力では面倒なのでプラグインに頼ります。
yarn add vue-multiselect
Vue-Multiselect | Vue Select Libraryのtaggingの項を参考に実装します。
<template>
<div class="field">
<label class="label">タグ</label>
<multiselect
v-model="tags"
tag-placeholder="Add this as new tag"
placeholder="Search or add a tag"
label="name"
track-by="name"
:code="options.tags.id"
:options="options.tags"
:multiple="true"
:taggable="true"
@tag="addTag"
></multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
/*form, categories*/
options: {
tags: [
{ name: 'Nintendo', id: 2 },
{ name: 'Microsoft', id: 3 },
{ name: 'Splatoon', id: 4 }
]
}
}
},
computed: {
post() {
return {
title: this.title,
categories: this.categories.id,
tags: this.tags.map(x => x.id).sort((a, b) => a - b),
content: this.content
}
}
},
methods: {
addTag(newTag) {
const tag = {
name: newTag,
id: this.options.tags.length + 300
}
this.options.tags.push(tag)
this.tags.push(tag)
}
}
}
</script>
v-bind:codeでidをcodeの代わりに使って重複の管理。
新規タグを追加する場合は、タグの個数+300でIDを振っています。これで、今後タグが増えてもIDが被ることはありません。
(※300足しているのは、WordPressで振られていたIDがタグ以外にもあるので重複しないように念を入れているだけなので、新規ブログとして作るのであれば1を足すだけで良いと思います)
意味はない気がしますが一応タグはID番号で並び替え。sortのアロー関数は配列に対してそのまま追記するだけなのに汎用性が高くて便利。
ここまで実装した結果、
こうなりました。
本文をマークダウンから変換
本文は最終的にHTMLタグとして送信する必要があります。タイトルと違って、見出しやリンクやiframe埋め込みなんかができなければ意味がないので。
HTMLタグを手打ちさせるブログなんか存在しないし、WordPressのようにキー操作に対応するのも手間なので、ここはマークダウン形式から変換しましょう。慣れない方はTyporaみたいなエディター使って最後に貼れば良いですし。
@nuxtjs/markdownitというモジュールが提供されていました。nuxt-community、痒いところに手が届きすぎていて大好きです。
modules/packages/markdownit at master · nuxt-community/modules
nuxt.config.js
に
`/*
** Nuxt.js modules
*/
modules: [
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@nuxtjs/markdownit'
],
markdownit: {
preset: 'default',
linkify: true, // URL文字列を自動でリンク化
breaks: true, // 改行を<BR>や<P>タグに自動変換
injected: true // 全ページで$md.renderを使えるようにする
},
を追記して、
computed: {
post() {
return {
title: this.title,
categories: this.categories.id,
tags: this.tags.map(x => x.id).sort((a, b) => a - ),
content: this.$md.render(this.content)
}
}
これだけで勝手に変換されます。
プレビュー
どうせならプレビュー画面も欲しいですね。
プレビューは今後記事閲覧ページでそのまま使うでしょうからコンポーネント化します。
nuxt.jsにおける「components」ディレクトリの規約(案)その1を参考にしつつ、components/pages/article.vue
というファイルを作ります。別にcomponents直下に作っても問題ないです。
<template>
<article class="box">
<div class="card-content is-paddingless">
<div class="tags is-marginless">
<a
v-if="post.categories"
class="tag my-category is-rounded is-marginless is-primary"
>
{{ options.categories.filter(x => x.id === post.categories)[0].name }}
</a>
<span class="tag is-marginless is-white"
><i class="material-icons my-small-icon">access_time</i
><time>{{ new Date().toLocaleString() }}</time></span
>
</div>
<p class="title has-text-link article-title is-size-4 is-size-5-mobile">
{{ post.title }}
</p>
<div class="content" v-html="post.content"></div>
<div id="post_tags" class="columns is-gapless">
<div v-if="post.tags" class="tags">
<span v-for="tag in post.tags" :key="tag" class="tag">
<i class="material-icons my-small-icon">tag</i
>{{ options.tags.filter(x => x.id === tag)[0].name }}
</span>
</div>
</div>
</div>
</article>
</template>
/* eslint vue/no-v-html: 0 */
export default {
props: {
post: {
type: Object,
default: () => ({})
}
}
}
当たり前ですがこの閲覧画面のテンプレートは適宜書き直してください。というかここに書いていないCSSとかもあります。
data() {
return {
mode: 'edit',
とモードの変数を設定して、
import MyArticle from '~/components/pages/article'
でコンポーネントを呼び出し、
components: {
Multiselect,
MyArticle
},
コンポーネントを登録し、
<template>
<div>
<div class="tabs">
<ul>
<li :class="{ 'is-active': mode === 'edit' }">
<a @click="mode = 'edit'">編集</a>
</li>
<li :class="{ 'is-active': mode === 'preview' }">
<a @click="mode = 'preview'">プレビュー</a>
</li>
</ul>
</div>
<template v-if="mode === 'edit'">
<article v-show="mode === 'edit'" class="box">
<!-- これまで作ってきたエディター画面 -->
</article>
</template>
<template v-if="mode === 'preview'">
<MyArticle :post="post" :options="options" />
</template>
</div>
</template>
今までのエディター画面を子要素にするコンテナを作り、
①エディターとプレビューを切り替えるタブ
②エディター(ここまで作ってきた画面)
③プレビュー
という要素を作ります。
プレビュー画面はWordPressから持ってきて微調整しています。
カテゴリーやタグのロジックもcomponent化した際にはcomputedにするべきですが、今はこれ以上変数を増やしたくなかったので直書きしました。
# 見出し1 ## 見出し2 ### 見出し3 #### 見出し4 ##### 見出し5 ###### 見出し6
段落 改行
改段落
– リスト1 – ネスト リスト1_1 – ネスト リスト1_1_1 – ネスト リスト1_1_2
> 引用
`npm run dev`
[Vue.js](https://jp.vuejs.org/)
↑上記の一連のコードを本文編集欄に貼り付けてみてください。
終わりに
これで一旦編集画面が完成しました。
実は画像のアップロードシステムというかなり面倒な問題が残っていますが、一旦忘れましょう。
次回はFirebaseの設定、Firestoreへのデータ送信です。
……薄々お気づきかと思いますが、余程の理由がなければContentfulなどの既存Headless CMSに頼った方が絶対に楽です。