スマートにiPhoneXの対応のためのpaddingを設定する

iPhoneXには、画面上部にセンサーや前面カメラが仕込まれているディスプレイの切り欠きがあります。

safariではviewportを下記のように指定することで、横持ちした際にこの切り欠きの周りまでレンダリング領域とすることができます。

<meta name="viewport" content="viewport-fit=cover">

この指定をすると、ディスプレイ全体に背景色等を引き伸ばせるため、より一体感のあるデザインにすることができるのですが、切り欠き部分がコンテンツと重なってしまうことが起こり得るため、CSS定数を使ってpadding等を余分に持たせてあげることで、これを回避してあげる必要があります。

設定できるCSS定数は以下の通り

constant(safe-area-inset-top)
constant(safe-area-inset-right)
constant(safe-area-inset-bottom)
constant(safe-area-inset-left)

しかし、このCSS定数で使われる constant 関数ですが、他のブラウザでは一切実装されていない機能になります。
そのため、既にpaddingが設定されている要素にcalc等を使って切り欠き分をプラスしてあげる方法だと、構文解釈ができずにpaddingが未設定とされてしまいます。
とはいえわざわざiPhoneXのために新しい要素で囲ってあげるのもバカらしい…。

そんなときは下記の様に同じセレクタ内でpaddingを複数記述するとスマートに解決することができます。

.container {
	padding: 10px 20px;
	padding-right: calc(20px + constant(safe-area-inset-right)); // for iPhone X
	padding-left: calc(20px + constant(safe-area-inset-left)); // for iPhone X
}

safariでは padding-rightpadding-left を上書きするように後述しているので、切り欠き分も余分に余白が適用されますが、それ以外のブラウザでは constant を使っている行は解釈されないので1行目のみ適用されます。

要は記述順が重要で、ベンダープレフィックスのような用法で使ってあげればよいのです。

iOS10のSafariではVideo要素のインライン再生が可能になった

iOS9までのSafariでは、基本的にvideoのインライン再生はできず、再生すると強制的にフルスクリーンになっていました。

iOS10からはvideo要素にplaysinline属性をつけてあげることでインライン再生が可能になってます。

<video src="path/to/video.mp4" controls playsinline></video>

※ただしiOS9でもWebViewでは webkit-playsinline でインライン再生できました。

そしてこの仕様に併せて、JSからvideoのフルスクリーン制御もできるようになってます。

const video = document.querySelector('video');

// フルスクリーンにする
if(video.webkitSupportsFullscreen){
  video.webkitEnterFullscreen();
}

// フルスクリーンから離脱する
if(video.webkitSupportsFullscreen){
  video.webkitExitFullscreen();
}

video以外の要素で webkitRequestFullscreen を使ってフルスクリーンするのはまだiOSでは解禁されていませんが、videoのみ webkitEnterFullscreen でフルスクリーンできるようになった、ということです

SSRしないNuxt.jsでページロード時に非同期部分を更新する方法

Nuxt.jsでは、外部サーバ等から非同期でデータを取得・表示する場合にも asyncData を使ってあげることでクライアントサイドはもちろん、サーバサイドでもデータを取得し、HTMLで出力することが出来ます。

SSRできる本番環境があれば何の問題もないのですが、SSRせずプリレンダリングのみで運用する場合には、 asyncData は静的ファイルをgenerateした時点でしか走っておらず、ページロード時にデータが更新されていない問題が起こり得ます。

例えばコーポレートサイト等で、別のサーバにWordPressが設置してあり、トップページのお知らせとしてWordPressから記事を取得するような場合を想定すると、こんな感じで作ってあげることで解決できます

// 非同期でデータを取ってくる関数
async function getPostData() {
  return { data } = await axios.get('https://blog.example.com/wp-json/wp/v2/posts');
}

export default {
  async asyncData(context) {
    const obj = {};
    if (context.isServer) { // asyncDataするのはサーバサイド(プリレンダリング時)のみ
      obj.posts = await getPostData();
    }
    return obj;
  },
  async created() {
    if (!this.$isServer) { // クライアントサイドはここで更新をかける
      this.posts = await getPostData();
    }
  },
};

要は asyncData を使うのはサーバサイド(プリレンダリング時)のみ。
クライアントサイドでは created でデータの更新をかけてあげるわけです。

こうすることでプリレンダリング運用でも、一応(少し古いかもしれないけど)非同期データのHTMLもプリレンダリングされていて、ブラウザで訪れた際にはちゃんと最新の情報に更新されます。

Vue.jsでSSRする場合にv-forでエラーが出る

Nuxt.jsを含めて、Vue.jsの2.xを使ってSSR(サーバーサイドレンダリング)をする場合に、 v-for の箇所で下記のようなエラーが出る場合がある。

The client-side rendered virtual DOM tree is not matching server-rendered content.

再現したコードはこれ( items は単純な文字列の配列)。
ちなみにVue.jsのバージョンは 2.2.6

<template v-for="item in items">
  {{ item }}
</template>

回避するには、下記のように個々を要素で囲ってあげる。

<template v-for="item in items">
  <span>{{ item }}</span>
</template>

クライアントサイドのみではエラーは出ず、ちゃんとレンダリングされるので、サーバ側の実行ロジックにバグがあるのかもしれない。

WP REST API では1回のリクエストでカテゴリーを全件取得することはできない

タイトルの通りですが、WP REST APIでカテゴリー情報を取得する場合、下記のエンドポイントを使いますよね。

/wp-json/wp/v2/categories

これに対してパラメータ per_page で取得件数を設定できるのですが、実はこのパラメータ、1〜100の数値以外を渡すとエラーになります。

ドキュメントにも取りうる値の範囲は記載されていません
https://developer.wordpress.org/rest-api/reference/categories/

なんとなく語感がget_postsでよく使う posts_per_page に似てるので-1を指定すれば全件取得できそうな気がするのですが、ところがどっこいできません。

おそらくパフォーマンスの問題で無茶な使い方を塞がれてるんだと思うのですが、100件以上になりうる可能性がある場合は、本当に必要なカテゴリーだけをパラメータ include で指定して取得してあげるほうが良さそうです。

それでも100件以上欲しい場合は、パラメータ page でしっかりロジカルにページングする必要がありそうです。

Prerender SPA Plugin を使ってSPAサイトのSEO対策をする

株式会社なすびのウェブサイトは、Vue.jsを用いたSPAで作られています(2017年4月現在)。

コーポレートサイトのような情報の提供に重きを置くサイトで、SPAのアーキテクチャを使うことはSEOの面からあまり適した用途であるとは言えません。

それは、GoogleボットこそJSを実行してからパースしてくれるので問題にはならないのですが、FacebookなどのSNSにシェアされた場合は未だにJSの実行までしてくれないからです。

これを解決するためには、サーバーからのレスポンスでHTMLを返す必要性が絶対的にあるのですが、いかんせんSSRを前提に環境の構築を考えると、サーバサイドでNode.jsを噛ませる必要があったりなど、なかなかに敷居が高いのです。

しかしコーポレートサイトのような、ページ数も動的コンテンツも少ない小規模サイトであれば、プリレンダリングという選択肢もあります。

今回はwebpackプラグインでサクッとプリレンダリングする方法の紹介です。

Prerender SPA Plugin
https://github.com/chrisvfritz/prerender-spa-plugin

このプラグインを使って、webpackのbuild過程で静的HTMLを書き出してしまいます。

// webpack.conf.js
var Path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')

module.exports = {
  // ...
  plugins: [
    new PrerenderSpaPlugin(
      // Absolute path to compiled SPA
      Path.join(__dirname, '../dist'),
      // List of routes to prerender
      [ '/', '/about', '/contact' ]
    )
  ]
}

上記のように保存先とエンドポイントを設定してbuildすると、下記3ファイルが書き出されます。

/index.html
/about/index.html
/contact/index.html

これらのファイルをウェブサーバーに配置すると、当然下記URLでbuildされたHTMLがレスポンスされるのですが、

http://example.com/
http://example.com/about/
http://example.com/contact/

SPAとしてリンクを辿ってaboutページに遷移すると、URLは /about となります。

/about/about/ は違うファイルを指しているため、この状態でリロードすると404になってしまいます。
これを回避するためには、 /about にリクエストが来たら /about/ へリダイレクトする.htaccessを置いてあげれば良いのです

<ifModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !index
RewriteCond %{REQUEST_URI} !.*\.(css|js|html|png|jpg)
RewriteRule (.*) / [L]
</ifModule>