Tumblr API + Vue.jsでTumblrテーマを作った話

初投稿です。

先日、僕のサークルのWebサイトをリニューアルしました。サイトは更新がかんたんなのでTumblrで作っています。リニューアル前はTumblr APIを使ってReactで作っていましたが、今回はVue.jsを使って作り直しました。そのときのもろもろをメモしておきます。


## 作ったサイト
~~[Dancng Girl.](http://dncngrl.com)~~
[2017.9.8 追記]
結局↑のサイトはTumblrをやめてWordPress(WP REST API) + Vue.jsに移行しました。


## 中の人について
Webデザイナー。


## なぜTumblr API + Vue.jsなのか
### Tumblrのテーマタグは自由度が低い
Tumblrはテーマタグが用意されていて、テーマカスタマイズ画面でHTMLを編集し、テーマタグを埋め込んだりすることでカスタマイズが可能です。

[Creating a custom HTML theme | Tumblr (日本語)](https://www.tumblr.com/docs/ja/custom_themes)

ですが、正直言って用意されているタグの種類も少なく、自由度が低いです。オリジナリティのあるWebサイトをTumblrで作りたい場合、かなり苦行だと思ってもらえれば結構です。  
テーマタグを記述するHTMLでは、トップページや下層ページ、各投稿タイプなども含めて、全て1つのHTMLに記述しなければならず、コードの見通しも極めて悪いです。

また、開発時にサイトをローカルで確認するのも面倒です。Middlemanと[Tumblargh](https://github.com/jasonwebster/tumblargh)というGemでTumblrテーマをローカルで開発することも[可能](http://memo.brdr.jp/post/101737595846/middleman-tumblargh-%E3%81%A7tumblr%E3%83%86%E3%83%BC%E3%83%9E%E3%82%92%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B)ですが、開発が数年前で止まっているGemで、現在では非対応のタグも多く、なかなか大変です。

### APIがあればなんでもできるからね
Tumblr APIを使うことで、「ローカルでTumblrのテーマタグが動かないから表示確認ができない」「1つのHTMLにすべてのタグを記述しないといけない」という問題から開放されます。自分の好きなように、サイトや記事の情報を取得し、表示すればいいだけだからです。

* [API | Tumblr](https://www.tumblr.com/docs/en/api/v2)
* [Tumblr APIでwebサービスを作りたい全ての人に向けて書きました](https://syncer.jp/tumblr-api-matome)

「小規模のサイトだったら気合で普通にテーマタグ書けば？」と言われるとその通りなので反論できませんが、それはそれ、ということで。  
また、Tumblr APIもテーマタグと同じく不備が多く、欲しい情報が取れなかったりします。そこは後述します。

### なぜVue.jsなのか
[vue.js](http://jp.vuejs.org/)

* AngularJSはチュートリアルくらいしか触ったことがない
* Reactはいくつかサイトを作ってみたけどあまり気に入らなかった
* Vue.jsはドキュメントも短くて読みやすくて比較的使いやすかった

という理由から、Vue.jsを使いました。


## Tumblr APIを使うにあたって
今回のようにTumblr APIをブログ記事の取得にのみ使う場合、必要なのはサイトのURLとAPI Keyです。API Keyは以下のURLから取得可能です。

[https://www.tumblr.com/oauth/register](https://www.tumblr.com/oauth/register)

この記事を参考にしたりしながら、アプリを登録してAPI Keyを取得します。

[Tumblr APIを使うためにAPIkeyを取得する | Stainless Note](http://stainless-note.tumblr.com/post/38512941758/tumblr-api%E3%82%92%E4%BD%BF%E3%81%86%E3%81%9F%E3%82%81%E3%81%ABapikey%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)


## Vue.jsでTumblrテーマを作る
Vue.jsはそのまま使うと、AngularJSのようにHTML内にデータバインディング構文を書いていくことになります。

### Vueifyとvue-loader
[Vueify](https://github.com/vuejs/vueify) (with Browserify)や[vue-loader](https://github.com/vuejs/vue-loader) (with Webpack)といったnpmパッケージを使うことで、`.vue`という拡張子のファイルの中に、ViewのHTMLタグおよびデータバインディング構文と、Vue.jsの処理を記述することができ、コンポーネント単位でコードを管理することができるようになります。Reactの`.jsx`ファイルのような感じです。

また、CSSも.vueファイルの中に書くことができます。が、僕はCSSは別で管理するのが好きなのでスタイルは.vueファイルには書きません。

僕はBrowserifyをよく使うので、Vueifyを使って開発していきます。

### Gulpをタスクランナーに使う
タスクランナーとして、Gulpを使います。ファイルの追加や変更を監視して、

* Browserify + Watchfyで`.js`、`.vue`ファイルのコンパイル
* Stylusのコンパイル
* 画像のMinify

などをおこないます。  
開発が一段落して実際にTumblrに反映するぞという場合は、

* JavaScriptの圧縮
* CSS内の画像ファイルへのパスを絶対パス(ファイルをホストしているURL)に書き換え
* CSSの圧縮
* HTMLにTumblrテーマタグを埋め込み(後述します)

などをおこないます。

各タスクの詳細は、[GitHubのリポジトリ](https://github.com/ryonakae/tumblr-dncngrl/tree/master/gulp)をご覧ください。

### ページ遷移時に任意の処理やアニメーションを実行する
Vue.jsのルーターである[vue-router](https://github.com/vuejs/vue-router)を導入しページ遷移を実装します。

そのときに、ページ遷移時(あるページに遷移するときやあるページから他のページに遷移するとき)に任意の処理やアニメーションを実行したい、という場合があると思います。そういった時は、vue-routerのトランジションフックを使うと便利です。

[トランジションフック | vue-router ドキュメンテーション npm package](http://vuejs.github.io/vue-router/ja/pipeline/hooks.html)

```js:source/javascripts/pages/News.vue
<template lang='jade'>
...
</template>

<script>
export default {
  // ... 他のオプション
  route: {
    activate: function(transition) {
      if (transition.from.path === '/work') {
        console.log('作品一覧からニュース一覧に遷移したよ');
      }
      if (transition.from.name === 'post') {
        console.log('ニュース詳細からニュース一覧に遷移したよ');
      }

      transition.next();
    },
    deactivate: function(transition) {
      if (transition.to.path === '/') {
        console.log('ニュース一覧からトップページに遷移するよ');
      } else if (transition.to.name === 'post') {
        console.log('ニュース一覧からニュース詳細に遷移するよ');
      }

      transition.next();
    }
  }
};
</script>
```

トランジションフックは引数として`transition`オブジェクトを受け取ります。`transition.to.path`などとすることで遷移にまつわるいろいろな情報を取得することができるので、それに応じて条件分岐して処理を実行します。

### コンポーネント間でデータのやり取りをするためにVuexを使う
例えばサイトのタイトルや記事を取得する関数などは、複数のコンポーネントでほぼ同じものを使いまわします。そのときにそれぞれのコンポーネントに同じコードを書くのは、冗長だし保守性も低いと思います。  
また、記事一覧コンポーネントの中である処理をして表示を変更したときに、ヘッダーコンポーネントの表示ステータスを変更したい、といったシチュエーションもあると思います。

そういった場合に使えるのが[Vuex](https://github.com/vuejs/vuex)です。Vue.js向けのFluxフレームワークです。  
VuexおよびFluxについて僕はそこまでよく分かっていないのですが、

* 使い回すようなStateは各コンポーネントでは持たない、Vuex側(Vuex Store)でStateを保持しておく
* コンポーネントはVuex Store上のStateを監視・表示しているだけ
* コンポーネントは「◯◯を実行するよ」という通知をするだけ、コンポーネントがStateを変更するわけではない
* VuexがStateを変更するような関数を実行する
* Stateが変わったら、監視していたコンポーネントが表示している値も変化する

という雑な理解をしています。だ、だいたい合ってるやろ……。

Vuexを使うことで、コンポーネント間でのデータのやり取りや関数の使い回しが楽になりました。

### Tumblr APIは固定ページの情報が取得できない
「Tumblr APIもテーマタグと同じく不備が多く、欲しい情報が取れなかったりします」と書きましたが、個人的に一番つらいのが、固定ページの情報を取得できないことです。つらい……。

たとえば「About」などの固定ページをWebサイトに置く場合がよくあると思いますが、そういう場合は`jQuery.ajax();`などを使ってむりやりページのコンテンツを取得し、表示するのがおすすめです。

```js:source/javascripts/pages/About.vue
<template lang='jade'>
.article {{{articleData}}}
</template>

<script>
window.jQuery = window.$ = require('jquery');

export default {
  // ... 他のオプション
  data() {
    return {
      articleData: {}
    };
  },

  ready() {
    jQuery.ajax({
      type: 'GET',
      url: 'http://dncngrl.com/about',
      dataType: 'html',
      timeout: 10000,
      success: (res) => {
        const article = $(res).find('#article').html();
        this.articleData = article;
      }
    });
  }
};
</script>
```

なかなか微妙な解決策ですが、Tumblr APIを使う場合はこれしかないかな、と思います。テンプレートにページ内容を直に書いてもいいかも知れませんが、Tumblrでは固定ページの内容はテーマカスタマイズ画面に記述します。固定ページの情報を変更した時に、テーマのテンプレートの方も編集しなければならないので、面倒です。

### momentで日付をフォーマットして表示する
Tumblr APIを叩いて投稿の日時(date)をリクエストすると、`2016-02-26 07:00:00 GMT`というような書式のデータが取得できます。[moment](https://www.npmjs.com/package/moment)を使って日付のフォーマットをおこないましたが、少し詰まったのでメモしておきます。

ChromeとOperaでは、以下のコードで問題なく日付が整形されます。

```js
import moment from 'moment';
const date = moment(new Date('2016-02-26 07:00:00 GMT')).format('YYYY.M.D');
console.log(date); // 2016.2.26
```

ですが、FirefoxとSafariだと、`Invalid Date`という表示になり、大変厳しい感じになります。

この解決策としては、`date`プロパティではなく`timestamp`プロパティを使います。timestampの値である`1456470000`といった数字はUnixタイムスタンプというもののようで、これをmomentでフォーマットすると、すべてのブラウザでいい感じに日時が表示できます。`moment.unix()`メソッドの引数に日付オブジェクトを入れます。

```js
import moment from 'moment';
const date = moment.unix(new Date('1456470000')).format('YYYY.M.D');
console.log(date); // 2016.2.26
```


## その他のTumblrテーマ開発にまつわるあれこれ
### サーバーサイドレンダリングについて
ReactやVue.jsといったフレームワークは、サイト読み込み時にJavaScriptが実行され、DOMが生成されたりします。なので、`$ curl hoge.com`や「ページのソースを表示」でソースを表示しても`body`以下が空っぽ、ということになります。

それを避けるために、サーバーサイドレンダリングをおこない、JavaScriptが実行できない環境でもサーバー側でHTMLを生成する必要があります。  
ですが、今回はTumblrのテーマカスタマイズ画面にHTMLを貼り付ける前提なので、サーバーサイドの処理などはおこなうことができません。

そこで、以下の方法がおすすめです。

* HTMLにはTumblrのテーマタグを普通のテーマ同様に記述しておく
* 検索エンジンなどのロボットがアクセスした場合は、テーマタグが実行されて情報が表示される
* 人間がブラウザでアクセスした場合には、JavaScriptが実行され`body`以下を全て書き換えるので、テーマタグで生成された情報は見えない

HTMLにテーマタグを記述する必要はありますが、タグにClass付けやスタイリングなどは一切必要ありませんし、セマンティックにタグを記述しておくだけで良いので、お手軽だと思います。

### Tumblrのテンプレートタグを本番環境でのみHTMLに流し込む
上記のサーバーサイドレンダリング用のテンプレートタグやOGPの`meta`タグをHTMLに記述する必要がありますが、ローカル開発時にそれらのタグがあると、表示エラーになったりします。なのでローカルではテーマタグは一切記述せず、本番環境向けのビルド時のみテーマタグをHTMLに流し込みたいです。

Gulpを使うと簡単で、[gulp-include](https://www.npmjs.com/package/gulp-file-include)というパッケージを使うと可能です。

まず、HTMLとテンプレートタグは以下のような感じです。

```html:source/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  @@if (env === 'development') { @@include('templateTag/development/meta.html') }
  @@if (env === 'production') { @@include('templateTag/production/meta.html') }
</head>
<body>
  @@if (env === 'production') { @@include('templateTag/production/template.html') }
</body>
</html>
```

```html:source/templateTag/development/meta.html
<meta charset="UTF-8">
<title>Dancing Girl.</title>
<meta name="description" content="description here.">
<meta name="viewport" content="width=device-width, maximum-scale=1.0">
```

```html:source/templateTag/production/meta.html
<meta charset="UTF-8">
<title>{block:PostSummary}{PostSummary} | {/block:PostSummary}{Title}</title>
<meta name="description" content="{MetaDescription}">
<meta name="viewport" content="width=device-width, maximum-scale=1.0">
{block:TagPage}
  <meta name="robots" content="noindex,follow">
{/block:TagPage}
{block:DayPage}
  <meta name="robots" content="noindex,follow">
{/block:DayPage}

<meta property="og:locale" content="ja_JP">
<meta property="og:description" content="{MetaDescription}">
<meta property="og:site_name" content="{Title}">
{block:IndexPage}
  <meta property="og:title" content="{Title}">
  <meta property="og:image" content="{image:Ogp}">
  <meta property="og:type" content="website">
  <meta property="og:url" content="{BlogURL}">
  <meta meta name="twitter:card" value="summary">
{/block:IndexPage}
{block:PermalinkPage}
  {block:Posts}
    {block:Text}
      <meta property="og:title" content="{block:PostSummary}{PostSummary} | {/block:PostSummary}{Title}">
    {/block:Text}
    {block:Photo}
      {block:Date}
        <meta property="og:title" content="{Year}.{MonthNumberWithZero}.{DayOfMonthWithZero} | {Title}">
      {/block:Date}
      <meta property="og:image" content="{PhotoURL-HighRes}">
    {/block:Photo}
    {block:Photoset}
      {block:Date}
        <meta property="og:title" content="{Year}.{MonthNumberWithZero}.{DayOfMonthWithZero} | {Title}">
      {/block:Date}
      {block:Photos}
        <meta property="og:image" content="{PhotoURL-HighRes}">
      {/block:Photos}
    {/block:Photoset}
  {/block:Posts}
  <meta property="og:type" content="article">
  <meta property="og:url" content="{Permalink}">
  <meta meta name="twitter:card" value="summary_large_image">
{/block:PermalinkPage}

<link rel="shortcut icon" href="{Favicon}">
<link rel="alternate" type="application/rss+xml" href="{RSS}">
<link rel="apple-touch-icon" href="{PortraitURL-128}">
```

Gulpのタスクは以下です。秘伝のGulpfileなので、謎にCoffeeScript……。

```coffeescript:gulp/tasks/include.coffee
gulp    = require 'gulp'
include = require 'gulp-file-include'

gulp.task 'include:development', ->
  gulp
    .src 'source/index.html'
    .pipe include
      prefix: '@@'
      basePath: '@file'
      context:
        env: 'development'
    .pipe gulp.dest 'build/'

gulp.task 'include:production', ->
  gulp
    .src 'source/index.html'
    .pipe include
      prefix: '@@'
      basePath: '@file'
      context:
        env: 'production'
    .pipe gulp.dest 'build/'
```

これで、`$ gulp include`を実行すると、`index.html`にHTMLが流し込まれます。

### CSSの画像のパスを変更する
Tumblrはテーマに必要な画像などのファイルをアップロードすることもできますが、使い勝手がよくありません。自分のサーバなどでファイルをホストするのがおすすめです。

ローカル開発時にはもちろん画像などはローカルにあるでしょうから、開発時はローカルのファイルをCSSから参照し、本番環境では外部サイトに置いたファイルをホストします。GulpでCSSに記述してあるパスを変換してあげると良いです。[gulp-replace](https://www.npmjs.com/package/gulp-replace)を使うとかんたんに可能です。

```stylus:source/assets/stylesheets/style.styl
.index_title
  background url('../images/index_logo.png') no-repeat
```

```coffeescript:gulp/tasks/stylus.coffee
gulp = require 'gulp'
stylus = require 'gulp-stylus'
replace = require 'gulp-replace'

gulp.task 'stylus', ->
  gulp
    .src 'source/assets/stylesheets/**/*.styl'
    .pipe stylus()
    .pipe replace '../', 'http://example.com/'
```

これで、`$ gulp stylus`を実行すると、パスが置換された以下のようなCSSが出力されます。

```css:build/assets/stylesheets/style.css
.index_title {
  background: url('http://example.com/images/index_logo.png') no-repeat;
}
```


## 終わりに
以上、Tumblr APIとVue.jsでTumblrテーマを作る場合の勘所のまとめ的な投稿でした。かなり長くなってしまい、申し訳ありません。

サーバーサイドレンダリング(をしないでテーマタグを記述する)や、Gulpのタスクなどは、Vue.js以外のフレームワークでTumblrサイトを作る場合にも使えると思います。

「けっこう面倒くさい感じだし、いやホント普通にテーマタグ書けば？」と思われた方、実にその通りです。  
でも、今回制作したサイトのようにシームレスな遷移を実現したいという場合、また例えば制作実績はWordPressで管理したいけどブログはTumblrでやりたい、などの要望がある場合は、APIを使ってJavaScriptフレームワークでサイトを作るのが、自由度が高くて良いと思います。

ここ最近はずっとTumblr APIでTumblrサイトばっかり作っていたので、次はWP REST APIを使って自分のWordPressのサイトをリニューアルする、などをやりたいと思います。