Feb 11, 2021

Lighthouseの点数が伸び悩むのでNuxtからGatsbyに変えてみた。点数も70点から92点に跳ね上がりました。

Lighthouse の点数が伸び悩むので Nuxt から Gatsby に変えてみました。点数も 70 点から 92 点に跳ね上がりました。詳しい手順は公式ページをみるとして Nuxt から Gatsby に移行に関連したものを中心に残しておきたいと思います。

まずは Gatsby 直後の Lighthouse の点数がこちらです。いきなりこの点数、Gatsby の効果は絶大だと感じました。

Lighthouse!

Gatsby

最初に Gatsby Starter Blog(https://www.gatsbyjs.com/starters/gatsbyjs/gatsby-starter-blog)に従いblogテンプレートからスタートです。

gatsby new my-gatsby-project https://github.com/gatsbyjs/gatsby-starter-blog

全体の構成みてみると Nuxt とそれほど全体の構成は変わらないかなと思いました。 Nuxt との違いはコンテンツを GraphQL から取り出すところとロジックと UI 部分が混ざっており Vue に慣れているとちょっと昔の Jquery 時代に戻ったような感じがしてしまうのは私だけでしょうか。

また画像ファイルの扱いがちょっと癖あるかなと思いました。これも慣れだと思います。全体の構成がわかったのでいよいよ Nuxt からの移植作業に取り掛かります。

コンテンツの移行

content の構成は記事ごとにフォルダを作りその中に index.md として作るようです。今回記事で使用する画像もそのフォルダに一緒にいれることにしました。また Nuxt の記事の日付は md ファイルの更新日を利用していましたが、gatsby ではフロントマターに date を設定しているようでした。調べるの面倒なので、サンプルに合わせて同じように日付をフロントマターに入れることにしました。合わせて heroImage 画面もフロントマターに今回設定することにしました。また prism の書き方も少し違うようなのでこれも合わせて変換していきます。

これを手作業で移動するのは難儀なので、以下のような簡単なスクリプトを作って移行することにしました。

transitionNtoG.sh
#!/bin/sh
 
# nuxt -> gatsby
# 記事ファイル名でフォルダを作成してその中に記事ファイルをindex.mdに変更して格納する。
# また記事の画像も同じフォルダに入れる。
# フロントマターにdateとheroImage画像名を追加する。
# 日付は記事ファイルの更新日、heroImageは記事ファイル名.jpg
# prism ヘッダ変更 ```language [タイトル] -> ```language:title="タイトル"
 
# input
org=./org   #記事フォルダ
heroImage=./heroImage  #heroImage画像フォルダ
img=./images  #記事で使用している画像フォルダ
 
# output
out=./out
for item in $org/*.md
do
    file=`echo ${item} | sed -e "s/\.md//g" | sed -e "s/\.\/org\///g"`
    fileDate=`date -r ${item} +%Y-%m-%dT%H:%M:%S.000Z`
    echo $file $fileDate
    mkdir $out/$file
    cp -a $item $out/$file/index.md
    cp -a $heroImage/$file.jpg $out/$file
    cp -a $img/$file* $out/$file
    sed -i '' -e "s/^description/date: ${fileDate}\nheroImage: ${file}\.jpg\ndescription/g" $out/$file/index.md
    sed -i '' -e "s/\(\`\`\`.*\).*\[\(.*\)\]/\1\:title\=\2/g" $out/$file/index.md
    sed -i '' -e "s/!](\/images\//!\]\(/g" $out/$file/index.md
done

Gatsby と plugin のインストール

今回、以下の plugin を入れることにしました。最初 gatsby-plugin-google-analytics を入れましたが、Google analystic V4 だとうまく行かなそうなので、手作業で Google が用意したスクリプトを追加しました。また prism のタイトルは gatsby-remark-prismjs-title を入れましたが気に食わなかったので、gatsby-remark-code-titles を利用することにしました。

yarn global add gatsby-cli
gatsby new akiboi-blog https://github.com/gatsbyjs/gatsby-starter-blog
yarn add node-sass gatsby-plugin-sass
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/react-fontawesome
yarn add @fortawesome/free-solid-svg-icons
~~ yarn add gatsby-plugin-google-analytics ~~
yarn add gatsby-plugin-robots-txt
yarn add gatsby-plugin-sitemap
 
yan add gatsby-transformer-remark
yarn add gatsby-remark-prismjs prismjs
~~yarn add gatsby-remark-prismjs-title~~
yarn add gatsby-remark-code-titles

あとは plugin に合わせて gatsby-config.js を修正すれば完了です。こちらは割愛します。あとは最初に作ったコンテンツを akiboi-blog/content/blog にコピーすれば OK です。scss ファイルはそのまま akiboi-blog/src/scss にコピーすれば OK です。scss ファイルを読めるように blog テンプレートを入れて作成されている layout.js に import してやれば OK です。

src/components/layout.js
import React from "react"
import PropTypes from "prop-types"
import { useStaticQuery, graphql } from "gatsby"
import "../scss/main.scss"
・・・

components の移行

テンプレートのソースを元に移植する方が早いと思います。vue で記載したものを return のところにスクリプトも一緒に入れるみたいな感じです。さほど難しいことはないと思います。動的ページは、gatsby-node.js 、templates そして componetns で実現します。ほとんど雛形がでできているのでこれに手を加える程度です。記事表示するサンプルを載せておきます。私の場合、gatsby-node.js はそのままサンプルのものを使い blog-posts.js は heroImage 属性を追加した程度でした。

src/blog-post.js
import React from 'react';
import { graphql } from 'gatsby';
import Article from '../components/Article';
 
const BlogPostTemplate = ({ data, location }) => {
  const article = data.markdownRemark;
  const { previous, next } = data;
  return (
    <>
      <Article article={article} previous={previous} next={next} location={location} />
    </>
  );
};
export default BlogPostTemplate;
 
export const pageQuery = graphql`
  query BlogPostBySlug($id: String!, $previousPostId: String, $nextPostId: String) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(id: { eq: $id }) {
      id
      excerpt(pruneLength: 160)
      html
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        heroImage {
          childImageSharp {
            fluid(maxWidth: 1024, quality: 100) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
    previous: markdownRemark(id: { eq: $previousPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        heroImage {
          childImageSharp {
            fluid(maxWidth: 1024, quality: 100) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
    next: markdownRemark(id: { eq: $nextPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        heroImage {
          childImageSharp {
            fluid(maxWidth: 1024, quality: 100) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`;

フロントマターに heroImage で指定した画像を表示するようにしています。

src/components/Article.js
import React from "react"
import Image from "gatsby-image"
import Layout from "../components/layout"
import SEO from "../components/seo"
import FooterNavi from "../components/FooterNavi"
 
const Article = ({ location, article, previous, next }) => {
  const title = article.frontmatter.title
  const description = article.frontmatter.description
  const html = article.html
  const heroImage = article.frontmatter.heroImage
  const f = true
  return (
    <>
      <Layout location={location} title="{title}>"
        <SEO title="{title} description={description} article={f} heroImage={heroImage} />"
        <div className="contents">
          <div>
            <Image fluid={article.frontmatter.heroImage.childImageSharp.fluid} />
          </div>
          <article
            className="blog-post"
            itemScope
            itemType="http://schema.org/Article"
          >
            <h1 className="title">{title}</h1>
            <section
              dangerouslySetInnerHTML={{ __html: html }}
              itemProp="articleBody"
            />
          </article>
          <hr />
          <nav className="blog-post-nav">
            <FooterNavi previous={previous} next={next} />
          </nav>
        </div>
      </Layout>
    </>
  )
}
export default Article

HamburgerMenu

HamburgerMenu も結局大した修正なしに移行することができました。最初は他サンプルとかみてましたが、なんでそんな複雑な処理するのだろうと悩んでしまいました。React ではステートフックがあるのでこれを利用して on、off がに合わせてメニューを開いたりできました。また class を追加する場合どうするのだろうと思ってましたが単純でした。on、off に合わせて is-active クラスを追加することができました。

Header.tsx
import React, { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHome, faServer, faNetworkWired, faGlobe, faIcons, faMortarPestle } from '@fortawesome/free-solid-svg-icons'
import { Link } from "gatsby"
import Logo from "./Logo"
 
export const Header = () => {
  const [open, setOpen] = useState(false)
  const menus = [
    {
      link: '/news/',
      icon: faHome,
      text: '新着',
    },
    ・・・
  ];
  return (
    <div className="menu">
      <nav>
      ・・・
        <div className="sp-menu">
          <div className="hamburger">
            <div className={"hamburger-line" + " " + (open ? "is-active" : "")} onClick={() => setOpen(!open)}>
              <span></span>
              <span></span>
              <span></span>
            </div>
          </div>
          {open && (
            <div className="hamburger-menu">
              <div className="logo"><Logo /></div>
              <ul className="menu-list">
                {menus.map(menu => (
                  <li><Link to={menu.link}><FontAwesomeIcon icon={menu.icon} /> {menu.text}</Link></li>
                ))}
              </ul >
            </div >
          )}
        </div>
      </nav>
    </div>
  )
}
export default Header

scss ファイルは変更なしでいけました。

header.scss
・・・ @include screen-mq(sm) {
  header {
    .menu {
      .pc-menu {
        display: none;
      }
      .sp-menu {
        display: flex;
        width: 100%;
        position: fixed;
        top: 0;
        left: 0;
        justify-content: center;
        .hamburger-menu {
          width: 100%;
          height: 100vh;
          background: $sp_background_menu_color;
          padding: 2rem 0;
          .logo {
            flex-direction: column;
            justify-content: center;
            padding: 1rem 0;
            width: auto;
            text-align: center;
            .wrapper {
              display: flex;
              justify-content: center;
              align-items: center;
            }
          }
          .menu-list {
            display: flex;
            flex-direction: column;
            align-items: center;
            li {
              list-style: none;
              padding: 1rem 0;
            }
          }
        }
        .hamburger {
          width: 60px;
          height: 60px;
          position: fixed;
          top: 0;
          right: 0;
          .hamburger-line {
            width: 36px;
            height: 30px;
            margin-top: 15px;
            margin-left: auto;
            margin-right: auto;
            position: relative;
            cursor: pointer;
            span {
              width: 100%;
              height: 2px;
              background: $main_font_color;
              display: block;
              transition: 0.6s;
              position: absolute;
              &:first-child {
                top: 0;
              }
              &:nth-child(2) {
                top: 14px;
              }
              &:last-child {
                bottom: 0;
              }
            }
            &.is-active {
              span {
                transition: 0.6s;
                &:first-child {
                  transform: rotate(45deg);
                  top: 50%;
                }
                &:nth-child(2) {
                  opacity: 0;
                }
                &:last-child {
                  transform: rotate(-45deg);
                  top: 50%;
                }
              }
            }
          }
        }
      }
    }
  }
}

あと微調整で prism 関連で少し手直して完了です。以上で移行は完了です。コードはもう少し綺麗にしたい気持ちはありますが、一通りこれで完了です。

コード 比較

最後にコード数を比較したいと思います。イメージは gatsby が少ないような気がするのですけど。

Nuxt

NoLanguagefilesblankcommentcode
1Sass762709
2Vuejs Component12022266
3TypeScript200115
4SUM216241090

Gatsby

NoLanguagefilesblankcommentcode
1Sass762740
2JavaScript102614498
3TypeScript581197
4SUM2240171435

結果は Gastsby が 345step も多くなりました。Saas は少し手直ししたので 31step はいいとして JavaScript が多いですね。ロジック自体は少ないと思いますが、GraphQL の部分が多いためかなと思います。今回は、テンプレートベースに移行しましたので、保守しやすいようにもう少しソースコードは見直したいと思います。

今回は、Nuxt を Gatsby に移行してみました。満足いくスコアを出せましたが、アプリ屋さんとしては全部 Typescript にしたいし、もう少し React と付き合いたいと思います。