Nuxt.jsとMDLで静的サイトを作る

# Nuxt.jsで静的サイトを作る

Middlemanで制作していたサイトのソースを紛失したので、いっその事Nuxtで作り直した話。

ちなみに作成したのは学校でやってる国際ワークショップのサイト。[ここ](http://beam.kisarazu.ac.jp/iweee/2017/)から見られます。今後、自分が卒業したあと別の担当者が管理する可能性があるので、極力タグの記述のみでスタイルが適用されることと、ヘッダーやフッターなどの年号を設定ファイルでまとめて管理するなど気を配りました。

[Github - ainehanta/iweee-site](https://github.com/ainehanta/iweee-website)

![iweee-v2.png](https://qiita-image-store.s3.amazonaws.com/0/50147/bcafe3e9-057c-0fc9-72db-3d6424ffc082.png)

## 雛形作成

まず[Nuxt.js公式](https://ja.nuxtjs.org/guide/installation)に従ってcliから雛形を生成しましょう。パッケージ管理はyarnを使います。

```bash
vue init nuxt-community/starter-template iweee
cd iweee
yarn install
```

今回はCSSフレームワークに[Material Design Lite](https://getmdl.io/)を使います。その他にも必要なパッケージ類をインストールしましょう。

```bash
yarn add material-design-lite node-sass sass-loader typicons.font
```

以上で、コーディングの準備が整いました。

## ファビコン

ファビコンを生成しましょう。[realfavicongenerator.net](https://realfavicongenerator.net/)が使いやすいのでおすすめです。生成したファビコンは`static`ディレクトリに配置しましょう。

## サイト設定

ヘッダーやフッターに記載されている開催年やCopyrightはまとめて設定ファイルで管理します。まずその設定ファイルを作成しましょう。また、このサイトはサブディレクトリで運用されるのでルートからのパスも設定します。

```json:iweee.config.json
{
  "year": "2017",
  "copyrightYear": "2017",
  "serverPath": "/iweee/2017/"
}
```

続いて、サイト全体の設定や`<head>`タグに関する設定はプロジェクトルートの`nuxt.config.js`で行います。ここで、先程の設定ファイルの内容を読み込みましょう。Webpackは`require`でjsonを指定すると自動的にObjectとしてロードしてくれるので便利です。

```js:nuxt.config.js
const { resolve } = require('path');
const iweee = require('./iweee.config.json');

module.exports = {
  head: {
    titleTemplate: `%s - IWEEE:International Workshop on Effective Engineering Education ${iweee.year}`,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: `IWEEE:International Workshop on Effective Engineering Education ${iweee.year} Official WebSite` },
      { name: 'theme-color', content: '#ffffff' }
    ],
    link: [
      { rel: 'apple-touch-icon', sizes: '180x180', href: 'apple-touch-icon.png' },
      { rel: 'icon', type: 'image/png', sizes: '32x32', href: 'favicon-32x32.png' },
      { rel: 'icon', type: 'image/png', sizes: '16x16', href: 'favicon-16x16.png' },
      { rel: 'manifest', href: '/manifest.json' },
      { href: 'https://fonts.googleapis.com/icon?family=Material+Icons', rel: 'stylesheet' },
      { href: 'https://fonts.googleapis.com/css?family=Roboto:300,400', rel: 'stylesheet' }
    ],
  },
  css: [
    { src: 'material-design-lite/src/material-design-lite.scss', lang: 'scss' },
    { src: 'typicons.font/src/font/typicons.css' },
  ],
  plugins: [ '~/plugins/global-mixins.js', '~/plugins/global-components.js' ],
  loading: { color: '#3B8070' },
  build: {
    extend (config, ctx) {
      if (ctx.dev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    },
    vendor: [ 'material-design-lite/material.min.js' ]
  },
  generate: {
    dir: resolve(__dirname, './dist' + iweee.serverPath),
  },
  router: {
    base: process.env.NODE_ENV === 'dev' ? '/' : iweee.serverPath,
  }
}
```
`head`要素は書いた内容がそのまま`<head>`タグに出力されるので説明は省きます。

`css`要素は前ページで利用したいスタイルを設定します（[公式リファレンス](https://ja.nuxtjs.org/api/configuration-css#css-プロパティ)）。`node_modules`のパッケージから読み込むときはパスをパッケージ名で始めてください。プロジェクト内のファイルを読み込む場合はプレフィックス`~`をつけたパスにしてください。

`plugin`要素は後述しますがNuxtプラグインを読み込みます。

`build`要素はビルドの設定です。ここで追加しているのは`vendor`要素です。各ページやコンポーネントで`node_modules`からのソースを読み込むとNuxtは各ページごとにビルドし、おなじソースコードが大量にpackされます。そこで全ページで利用する`node_modules`からのソースはここに記述することになっています（[公式リファレンス](https://ja.nuxtjs.org/api/configuration-build#build-プロパティ)）。

`generate`要素は静的サイトとして生成するときの設定です。ここでは設定されたサブディレクトリで出力されるようにします。

`router`要素はvue-routeの設定です。ここもサブディレクトリ運用のための設定です。開発時はルートで、運用時はサブディレクトリでリンクが生成されます。

## プラグイン

Nuxtを書いていると、`Vue.component`だったり`Vue.use`を使いたくなります。そういうときはプラグインを使います。このサイトではサイト設定の注入と全ページで利用したいコンポーネントの読み込みに使ってます。

サイト設定の注入には`Vue.mixin`を使いました。

```js:plugins/global-mixins.js
import Vue from 'vue'
import iweee from '../iweee.config.json'

Vue.mixin({
  data () {
    return {
      iweee: iweee
    }
  }
})
```

グローバルコンポーネントはおなじみ`Vue.component`です。

```js:plugins/global-components.js
import Vue from 'vue'
import PageArticle from '~/components/PageArticle.vue'

Vue.component('PageArticle', PageArticle)
```

## レイアウトと基本的なコンポーネント

続いて、レイアウトと基本的なコンポーネントを作成しましょう。

基本コンポーネントはドロワー、ヘッダーとフッターの合計３コンポーネントです（勝手に決めた）。これらとコンテンツをレイアウトで結合します。

### レイアウト

実際のレイアウトファイルです。各コンポーネントをロードして配置しているだけなのでシンプルです。コンテンツは`<nuxt>`タグに展開されます。

```html:layouts/default.vue
<template>
<div>
  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
    <NavigationHeader/>
    <NavigationDrawer/>
    <div class="mdl-layout__content">
      <nuxt/>
      <NavigationFooter />
    </div>
  </div>
</div>
</template>

<script>
import NavigationDrawer from '~/components/NavigationDrawer.vue'
import NavigationHeader from '~/components/NavigationHeader.vue'
import NavigationFooter from '~/components/NavigationFooter.vue'

export default {
  components: {
    NavigationDrawer,
    NavigationHeader,
    NavigationFooter
  }
}
</script>
```

### ドロワー

画面の左端にあるやつです。Nuxtはページ切り替え時にDOMを再構築しないので速いみたいですが、MDLはそんなこと想定してないのでモバイル表示の時にドロワーのリンクをクリックすると新しいページに遷移してもドロワーが開いたままになります。そこで`<nuxt-link>`のクリックイベントをフックして強制的に閉じます(mdl側にはtoggleDrawerしかなかったのでコピーして閉じる動作だけにしてあります)。あと`@click.native`じゃないとnuxt側にイベント持ってかれちゃって取れないです。

```html:components/NavigationDrawer.vue
<template>
  <div class="mdl-layout__drawer">
    ...
    <nav class="mdl-navigation">
      <nuxt-link to="/" class="mdl-navigation__link typcn typcn-home iweee-navigation__link--home" @click.native="closeDrawer">Home</nuxt-link>
      ...
    </nav>
  </div>
</template>

<script>
export default {
  methods: {
    closeDrawer: () => {
      document.querySelector('.mdl-layout__obfuscator').classList.remove('is-visible')
      document.querySelector('.mdl-layout__drawer').classList.remove('is-visible')

      document.querySelector('.mdl-layout__drawer').setAttribute('aria-hidden', 'true')
      document.querySelector('.mdl-layout__drawer-button').setAttribute('aria-expanded', 'false')
    }
  }
}
</script>

<style scoped>
...
</style>
```

### ヘッダー、フッター

特に説明しなくても大丈夫だよね…？

```html:components/NavigationHeader.vue
<template>
  <header class="mdl-layout__header mdl-layout__header--transparent">
    <div class="mdl-layout__header-row mdl-shadow--2dp">
      <span class="mdl-layout-title">
        <img src="~/assets/img/iweee-logo-line.png">
      </span>
    </div>
  </header>
</template>

<style>
.mdl-layout__drawer-button {
  color: black !important;
}

.mdl-layout-title {
  display: block !important;
  position: absolute;
  left: calc(50% - 68px);
  top: 12px;
}

.mdl-layout-title img {
  width: 150px;
}

@media all and (min-width: 1025px) {
  .mdl-layout__header {
    display: none;
  }
}
</style>
```

```html:components/NavigationFooter.vue
<template>
  <footer class="copyright mdl-typography--text-center mdl-color-text--grey-600"><small>&copy; National Institute of Technology, Kisarazu College {{ iweee.copyrightYear }}</small></footer>
</template>

<style>
.copyright {
  position: relative;
  top: -50px;
}
</style>
```

## ページコンポーネントとコンテンツ作成

### ページコンポーネント作成

ページのレイアウトをコンポーネントに押し込めましょう。[名前付きスロット](https://jp.vuejs.org/v2/guide/components.html#%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88)でやりましょう。そうそう、`<slot>`の子要素に記述するとデフォルト値になるってこと初めて知りました。

ここでもNuxtとMDLの相性問題で、ページが遷移してもスクロール位置がリセットされません。本来は`scrollToTop:true`でできるんですが、うまく動きません（[公式リファレンス](https://ja.nuxtjs.org/api/pages-scrolltotop#scrolltotop-プロパティ)）。また無理やり`mounted`でスクロール位置をリセットします。

```html:components/PageArticle.vue
<template>
  <div>
    <div class="ribbon">
      <slot name="ribbon-img"><img src="~/assets/img/sogo-blur.png"></slot>
    </div>
    <div class="mdl-grid">
      <div class="mdl-cell mdl-cell--1-col mdl-cell--hide-tablet mdl-cell--hide-phone"></div>
      <article class="container mdl-cell mdl-cell--10-col mdl-shadow--4dp mdl-color--white">
        <div class="title">
          <slot name="title"></slot>
        </div>
        <div class="content mdl-color-text--grey-700 ">
          <slot name="content"></slot>
        </div>
      </article>
    </div>
  </div>
</template>

<script>
export default {
  mounted () {
    window.componentHandler.upgradeDom()
    document.querySelector('.mdl-layout__content').scrollTop = 0
  }
}
</script>
...
```

### コンテンツ作成

さあ、準備が整ったのであとはコンテンツを作成しましょう。Nuxtは`pages`ディレクトリにVueファイルを作成すれば自動的に認識してレイアウトとルーティングを作成します。

一番シンプルなページのサンプルを載せておきます。

```html:pages/program-schedules.vue
<template>
  <PageArticle>
    <h4 slot="title">Program Schedule</h4>
    <div slot="content">
      <div class="steps">
        <section class="step">
          <h4 class="step-head">Wednesday, 6th December 2017</h4>
          <div class="step-content">
            <p>Oral Presentation</p>
          </div>
        </section>
        <section class="step">
          <h4 class="step-head">Thursday, 7th December 2017</h4>
          <div class="step-content">
            <p>Students' Poster Presentation</p>
          </div>
        </section>
      </div>
      <p class="mdl-typography--text-center">(details will be updated later)</p>
    </div>
  </PageArticle>
</template>

<script>
export default {
  head: {
    title: 'Program Schedules'
  }
}
</script>
```

さっき、`nuxt.config.js`で`title`に`%s`が紛れてたと思いますが、各ページの`script`に`head`の`title`要素に値を設定すると置き換えます。便利です。

### staticディレクトリの行方

webpackで処理しないファイルは`static`ディレクトリに保存しますがビルドすると、直下にすべて展開されるので`static`の中へのリンクは`~static/hoge`ではなく`hoge`です。意外とつまづきます。

# ビルド

全部終わったら、静的サイトを生成しましょう。

```bash
yarn run generate
```

# 最後に

SPAにもいいけど、普通のサイト制作にも使えるやつです。いわゆるユニバーサルJSで書かなきゃいけないことがありますが、慣れれば楽です。

Nuxt.js初めて使ったけど、セクシーです。みんな使おう!