Edited at
Vue.js #1Day 23

ポッドキャストサイトをJekyllからNuxtでモダンに作り直した知見を余さず全部書く

More than 1 year has passed since last update.

本記事は Vue.js #1 Advent Calendar 2017 の23日目の記事です。


TL;DR



  • soussune(そうっすね)という技術系ポッドキャストのサイトをJekyllで構築してます

  • よくVueの話をしてるのに肝心のサイトでは使ってない。のでVue使いたい

  • Nuxt.jsにNuxtentモジュール入れて、nuxt generate で静的サイトを生成してデプロイしました

ポッドキャストをやっている @trkw@miyaoka の二人でこの一週間くらいスクラッチでサイトを作ってました。そこで実際にNuxt使ってみた話についていろいろ書いていこうと思います。


使用前・使用後

開発は8,9割程度終わった感じですが、現状まだ新サイトに移行してない状況です。

→移行しました。


旧サイト


新サイト


Jekyllとは


サイト構成

全体の構成はこんな感じのサイトです。

今回は、とくに打ち合わせもせずに二人で適当にやりたいところを実装してdiscordで話しながら進めていきました。なので、それぞれの話を書いていきます。


@trkwサイドの話: Nuxt化への道のり


Nuxtentで静的サイトを作る

今回soussuneでは Nuxtent を利用してサイトを構築しています。Jekyll、Hexoやその他の静的サイトジェネレーターを使うくらい簡単にコンテンツを扱うことができるツールです。始めるには、vue-cliでnuxtent-starterのテンプレートを使用します。

vue init nuxt-community/nuxtent-starter my-site

$ cd my-site
# install dependencies
$ npm install # Or yarn install

既存の環境に構築したい場合は、以下のように、nuxt.config.js内のmodulesに'nuxtent'を追加してもOKです。

module.exports = {

modules: [
'nuxtent'
]
}

ただし、@nuxtjs/axiosとは依存関係にあるので、こちらのインストールもする必要があります。

soussuneでは、nuxtent.config.jsで以下のようにepisodeとactorsのルーティングを定義しています。

const externalLinks = require('markdown-it-link-attributes')

module.exports = {
content: [
[
'episode',
{
page: '_id',
permalink: '/episode/:slug',
generate: [
// for static build
'get',
'getAll'
],
isPost: false
}
],
[
'actors',
{
page: '_id',
permalink: '/actors/:slug',
generate: [
// for static build
'get',
'getAll'
],
isPost: false
}
]
],
parsers: {
md: {
use: [
[
externalLinks, {
attrs: {
target: '_blank',
rel: 'noopener'
}
}
]
]
}
}
}

(※エピソードのルーティング、本当は複数形の /episodes/:slug にしたかったのですが現行サイトで /episode になっちゃってるのでそれを引きずってます)


ポッドキャストに必要なRSS機能が無いので、実装する対応


feedテンプレート

以下のようにNuxtのIssueをあげている人もいました。このIssueをあげられていた方は、@nuxtjs/sitemapや独自の実装を加えながら、atom.ejsにfeedのテンプレートを作っていてそこにデータをいれているパターン。

僕らもYattecastを利用していたので、Jekyllではこのatom.ejsのようにテンプレートを修正して対応していました


modules

modulesの作り方を勉強したので、テンプレートを意識せずnuxt.config.jsでObjectのみで管理しておきたいと思いました。nuxt.config.jsで管理すれば、Build時にFeed更新いけるんじゃないかとか考えながらsoussuneでは以下のように実装しました。

nuxt.config.jsには以下のようなObjectを作ってます。これは、npmのrssのサンプルに近い形で書いてます。

rss: {

title: 'soussune - エンジニアわいわいポッドキャスト「そうっすね」',
description: 'テクノロジーと世の中についてエンジニア達が雑談するポッドキャストです。',
feed_url: 'https://soussune.com/feed.xml',
site_url: 'https://soussune.com/',
image_url: 'https://soussune.com/images/itunes-artwork.jpg',
webMaster: 'soussune.user@email.com (soussune)',
managingEditor: 'soussune.user@email.com (soussune)',
copyright: '2017 soussune',
language: 'ja',
pubDate: 'Thu, 01 Jun 2017 04:00:00 GMT',
ttl: '60',
custom_namespaces: {
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
'itunesu': 'http://www.itunesu.com/feed'
},
custom_elements: [
{ 'itunes:subtitle': 'エンジニアわいわいポッドキャスト「そうっすね」' },
{ 'itunes:author': 'そうっすね制作委員会' },
{ 'itunes:summary': 'テクノロジーと世の中についてエンジニア達が雑談するポッドキャストです。' },
{ 'itunes:keywords': 'soussune, tech, technology, keyboard, web, development, developer' },
{ 'itunes:owner': [
{ 'itunes:name': 'そうっすね制作委員会' },
{ 'itunes:email': 'soussune.user@gmail.com' }
]},
{ 'itunes:image': {
_attr: {
href: 'https://soussune.com/images/itunes-artwork.jpg'
}
}},
{ 'itunes:category': [
{_attr: {
text: 'Technology'
}},
{ 'itunes:category': {
_attr: {
text: 'Tech News'
}
}},
{ 'itunes:category': {
_attr: {
text: 'Software How-To'
}
}}
]},
{ 'itunes:explicit': 'no' }
]
},
rssItems: episodes.episode,

各rssItemsついて


成果物

https://soussune.netlify.com/feed.xml

最終的にこのような形で出力されます。



@miyaokaサイドの話: やりたい機能を作る


実装したUIその1: (独善的な)音声プレイヤー 🔈

今回の目玉機能ですね。ポッドキャストサイトなので主機能として音声プレイヤーがあります。普通は各ページごとにプレイヤーのインスタンスを配置しますが、これをサイト全体でシングルトン化しました。

要は SoundCloud 的なものと言えば分かりやすいでしょうか。あのサイトでは音源を再生するとボトム固定のプレイヤーで再生されます。それにより再生されたままの状態で他のページに遷移することができるので、サイト全体の音源を練り歩く体験性が実現されています。

海外のポッドキャストサイトを探してみたところ、このサイトなども上部に固定のプレイヤーとなっており同様の感じです。しかし、これが良いかというとユーザーの慣れや分かりやすさの問題が大きいため、独りよがりなデザインにならないようにという点には気をつける必要はあると思います。ただ今回は機能性を追求するとこうせざるを得ないなという思いがありました。

Webのプレイヤーというものは再生中にリンクをクリックしたりページ遷移してしまうと、いともたやすく再生がオワってしまいます。現行サイトでもIssueにしてました。このへんRebuildなんかだとイベントハンドリングしてちゃんと確認ダイアログ出してますね。そうなんです。確認出せばいいんです。でも結局このときはリンクを全部新規タブで開くように設定したので、とりあえず問題にならなくなりました。

で、改めて今回作ろうとなったとき、果たして確認を出せば良いのかという意識がありました。そうする必要が無いならそうする必要が無いように作ろうと。あと、とりあえずNuxtでサイト立ち上げてみたら、当たり前ですが現行サイトより遷移が速いので、ガンガン遷移しまくりたい気持ちが芽生えました。そうしたらもう、止められないですよね。。。

なのでプレイヤーはシングルトンで固定。ユーザーの遷移したい気持ちを妨げてはいけないでしょう。なんかいい話っぽいですが、ユーザーというか自分ですね。自分の気持ちが大事です。


プレイヤーとコントローラー

SoundCloudを見ればわかりますが、固定のプレイヤーがあり、さらに各ページ内にもプレイヤーがあります。そしてこの両者は同じ音源を操作、状態表示してます。つまり一つのプレイヤーと複数のコントローラーということになります。最初は単にプレイヤー兼コントローラーを作っていたのですが、分ける必要が出てきたのでそんな作りにしました。

そのへん繋ぎこむのは、そうですね。そうです。その通り。Vuexですね。よしなにやりましょう。



(開発途中のもの。2つあると分かりづらいので記事内では再生停止のみのボタンにした)


プレイヤー自作

これまでサイトに音声プレイヤー配置したいって言ったら、とりあえずググって良さげなライブラリ使うってのが普通だと思うんですが、HTML5のaudio要素にsrc指定するだけで再生できるわけだし、その制御周りを作るのはまさにVueの得意なところだと思うんですよね。

controls属性をつければブラウザベンダの標準コントローラーを表示して操作もできます。しかし、これも以前からの課題に思っていることがありました。


  • 再生ボタンが小さいので大きくしたい

  • シークバーはスマホでも操作しやすいように画面幅一杯というUIにしたい

というあたりですね。

特にポッドキャストの場合だと音源が2,3時間とか長い場合もあるので、シークバーが短いと狙ったところを再生するのが難しいのです。なのでまあ、シークバー→でっかく、再生ボタン→でっかく、という意識が強かったので作っていくことにしました。


アプリ志向

とはいえ、なんていうか、そもそもなんですが、自分はwebではあんま再生してないんですよね…。今どきは基本スマホアプリで聴くものですし、アプリのほうが楽なんですよね。速度コントロールとか、15秒巻き戻したりとか。そうした操作に特化して設計されているのがアプリの強さです。

(iPhone標準のポッドキャストアプリでも十分ですが、サードパーティのOvercastとかだとチャプター機能にも対応していてもっと使いやすいです。うちのサイトもチャプター情報まで読み取れるか分からないですが、できたらチャプター表示と遷移の対応をしたいです)

でもWebってアプリじゃないですか。いや ドキュメント ですけど。Nuxtで作るってことはもうアプリじゃないですか。そこはもはやアプリという戦場に居るわけで、いつまでもドキュメント気分で居たら死んじゃうと思うんですよね。

でも、昔FlashやっててFlashサイト作ってたときはほんとアレでしたね。みんなただ独善的なアプリを作りたかっただけなんですよね。ドキュメントの気持ちなんて考えずに…。ドキュメントの心はすっかりもてあそばれて傷つきました。ユーザーさんも、僕らFlasherも傷つきました。あの時代を経て、HTML5、標準化。いい時代になったと思います。今ならドキュメントのこともしっかり考えられるくらいみんな大人になったと思います。我々が求めているのはドキュメントであり、そしてアプリなのです。

なのでまあwebで再生したくなるようなものを作っていけたらいいじゃないですか。作りましょうよ。


実装したUIその2: 検索機能 🔎


何故探すのか

サイトに検索機能ほしいなと思いました。

でもほんとに欲しいですか? 僕の場合いくつか理由があります。


  • 単純にログが増えてきたので探したい

  • 検索があればナビゲーションを作らなくて済む

  • クライアントサイドで全部やれるんだからやりたい

検索対象としては、「タイトル、本文、トピック、出演者」といったデータをStoreに持っているので、そことマッチさせるだけです。


検索欄

話題になった dev.to のレイアウトを見ると、ヘッダ中央に検索欄ありますよね。なんかもうこれだけで、意味、感じちゃいますよね。感じられてますか、意味。つまりそこがナビゲーションの起点だというアレを感じる感じですね。ブラウザが検索欄を起点としてるのと一緒だと思います。

これまで検索っていうと、とりあえずなんか右上とかに添え物的に置かれててどうにも本気で使う気力が感じられなかったイメージがあるんですけど、やっぱそこは中央にドンと置いて主張するのがモダンなのかなあという気はします。

で、ナビゲーションとするなら徹底しましょう。

今回は、どのページに居ても検索内容が入力されたらシームレスに記事一覧画面に遷移して、検索結果を表示するようにしました。このへんの遷移について作る前はあんまりイメージしきれていなかったんですが、とりあえず作っていったらああこれでいいじゃないとなったので、やはり気軽に作ってしまえる環境は楽だなあという感じはします。

そもそもの意識としては検索結果というよりただのフィルタリングですね。なので特別に検索結果画面というものを作るのではなく、記事一覧画面に対して動的なフィルタをかけるというものになります。そしてその状態をシェアできるように内容変更時にrouterのクエリも書き換えて、サイト初期化時にクエリから再現できるようにしてます。そうすることでこのように 「 vueについて話した回 」 といった感じにURLでサクッと状態共有できるようになります。

いやー前からこういうのがやりたかったっていうか、ドキュメントでありアプリって感じじゃないですか。


検索への重い想い

さっき言った、サイトの検索は添え物的って感覚は、検索は重いっていうところがあるのかなと思います。何が重いかって言うと、


  • ただでさえユーザーがマウスから手を離してキーボードを叩く労力を支払うし、

  • 検索待ちに要する時間、

  • そして期待ほどマッチしない結果…。

それだったら要らないですよね。

でもフィルタリングは便利です。即座に応答して、マッチしてなければマッチするように変えていける。コマンドラインのフィルタリングツールのpecoなんかは恩恵を受けている人も多いのではないでしょうか。dev.toを始めとして速さの追求が体験性として大きく取り沙汰されている昨今、クライアントサイドでやれるところはやっていきたいですね。

そんなわけでプレイヤーはボトム、検索はトップと、上下それぞれに機能を実装してアプリ的なサイトになったかなあという感じがします。


実装上のつまづきどころ、知見を振り返る


ルーティング


同じpagename間では遷移時のpage transitionが発動しない


  • 前後記事に遷移する場合など。しばらくハマってた。

  • ユニークなkeyをつけて読み直しが発生するようにする




  • 実装したもの


    • keyにfullpathを使っていたがこれだと検索時のクエリ変更でも遷移が発動してしまうのでパスのみにした




ページ内リンクするための#hashの文字


ページ間でページの前後を判断して左右スライドするトランジション設定

  transition (to, from) {

return (!from || from.name !== to.name)
? 'page'
: Number(to.params.id) < Number(from.params.id) ? 'slide-right' : 'slide-left'
}


遷移時にデフォルトではスクロール位置がそのまま(children routesがある場合)


再生プレイヤー


<input type="range">でシークバーを作る際のタッチ対応


  • input rangeは便利だけど、モバイルではつまみ部分しかタッチ対応しておらず、トラック部分のタッチで動かないのが不便。ポンコツ

  • input range touchでググるとだいたいjQuery製のライブラリ。そんなに機能は求めていないんだ…。

  • やるべきことはtouchstarttouchmoveのイベントハンドリング。それでタッチ座標とrange内容から値を算出できるので、スライダーのvalueを更新する


  • 実装したもの


    • これだと横位置固定なので、広く対応するならもうちょい厳密に書く必要はある




<input type="range">のスタイリング、クロスブラウザ対応


HTMLAudioElementのイベントハンドリング


SPA


ページ遷移してきた場合、embedされたツイッターウィジェットは再度読み込む必要がある

  mounted () {

this.loadTwitterWidget()
},
updated () {
this.loadTwitterWidget()
},
methods: {
loadTwitterWidget (): void {
if (window['twttr']) window['twttr'].widgets.load( 対象のDOM )
}
}


検索


nuxtentで本文内容が入って無くて検索できない


  • comp.mdだと本文の代わりにrelativePathになるのでcompつけないようにして対応


インクリメンタルサーチ時のIME対応



  • inputイベントはIME変換前の入力中でも発火するのでそれで検索かけると不都合なところがある


    • サーバーにリクエスト投げる場合は無駄なコストが発生する

    • 確定前文字列で検索してしまうので結果がマッチせずゼロ件表示になってしまう




  • IME対応なし



    • 変換されてない段階の入力内容で検索してしまう




  • IME対応あり



    • 変換が済んだタイミングで発火して検索




  • 実装したもの



  • でも結局、入力欄とは別に現在の検索中内容を表示するようにしたら一時的にゼロ件表示になっても違和感無いように思えたのでinputイベントのみにした



CSS


  • grid layout子要素のmax-widthがきかない


    • max-widthではなく、gridのほうでminmax()を指定する




その他これから