JavaScript
vue.js
MaterialDesignLite
nuxt.js

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

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

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

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

Github - ainehanta/iweee-site

iweee-v2.png

雛形作成

まずNuxt.js公式に従ってcliから雛形を生成しましょう。パッケージ管理はyarnを使います。

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

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

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

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

ファビコン

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

サイト設定

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

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

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

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要素は前ページで利用したいスタイルを設定します(公式リファレンス)。node_modulesのパッケージから読み込むときはパスをパッケージ名で始めてください。プロジェクト内のファイルを読み込む場合はプレフィックス~をつけたパスにしてください。

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

build要素はビルドの設定です。ここで追加しているのはvendor要素です。各ページやコンポーネントでnode_modulesからのソースを読み込むとNuxtは各ページごとにビルドし、おなじソースコードが大量にpackされます。そこで全ページで利用するnode_modulesからのソースはここに記述することになっています(公式リファレンス)。

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

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

プラグイン

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

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

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

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

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

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

Vue.component('PageArticle', PageArticle)

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

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

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

レイアウト

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

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側にイベント持ってかれちゃって取れないです。

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>

ヘッダー、フッター

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

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>
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>

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

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

ページのレイアウトをコンポーネントに押し込めましょう。名前付きスロットでやりましょう。そうそう、<slot>の子要素に記述するとデフォルト値になるってこと初めて知りました。

ここでもNuxtとMDLの相性問題で、ページが遷移してもスクロール位置がリセットされません。本来はscrollToTop:trueでできるんですが、うまく動きません(公式リファレンス)。また無理やりmountedでスクロール位置をリセットします。

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ファイルを作成すれば自動的に認識してレイアウトとルーティングを作成します。

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

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.jstitle%sが紛れてたと思いますが、各ページのscriptheadtitle要素に値を設定すると置き換えます。便利です。

staticディレクトリの行方

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

ビルド

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

yarn run generate

最後に

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

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