ブログをWordpress+ロリポップからGatsbyJS+Netlifyに移行した

Reactもご無沙汰だしGatsbyも初めてなのでメモりながらがんばっていくぞ!という記録 WP→Gatsby完全移行をお考えの方の参考になれば。

全体の流れ

  • 記事を移行する
    • プラグインを利用してmarkdown形式にする
    • markdownになりきれてない部分を置換・修正
  • GatsbyJSでgatsby-theme-blogを利用したフロントを作成する
    • 記事一覧ページに追加
      • 記事のタグ・カテゴリの表示
      • ページャー
    • タグ・カテゴリー別一覧ページを追加
    • 記事詳細ページに追加
      • 記事のタグ・カテゴリの表示
      • コメント機能(Disqusの表示)
      • 選択箇所を引用しTwitterでシェアする機能
    • Google Analyticsの設定
  • github
    • リポジトリ作成してソース一式push
  • Netlify
    • Githubのリポジトリと連携してビルド設定する
    • ドメインの設定

記事の移行

記事のエクスポートはpluginにお世話になりましたが、イマイチ期待通りとはならず以下修正。 幸いにも(?)記事数が少なかったのでほとんど手作業で対応してしまった。

以下はプラグインで対応

記事の移行 WP Gatsby Markdown Exporter
WP Gatsby Markdown Exporter – WordPress プラグイン | WordPress.org 日本語

URL一覧の出力  Export All URLs
Export All URLs – WordPress プラグイン | WordPress.org 日本語

メディアをuploads以下じゃなくて記事と同じディレクトリに置きたい

量が多かったらシェルスクリプト書くかなと思ったけど、そんなに画像が存在しなくて手で移動した。

slideshare, github gist, youtubeなどなど埋め込み系

以下のプラグインを使う事にした。

gatsby-remark-embedder
gatsby-remark-embed-gist

amazonへのリンク

これも数が少なかったので手動で書き換え。 WPのプラグインで出力されていたものを置き換えた。

シンタックスハイライトにプラグインを使っていた名残

<pre class="lang:default decode:true ">というシンタックスハイライト用のタグが残ってしまったので、テキストエディタの置換などを利用して手作業で変更していく。 md形式のシンタックスハイライトは言語ごとに名前を入れていかないといけないのでちょっとめんどくさい

コメントの引継ぎ

記事が間違っていたりする可能性が非常に高いのでコメント機能は引き続き欲しいよねと思ってDisqusを導入することにした(後述) 既存コメントについては件数も少なかったので引継ぎはナシということにしました。 (コメントくださった方々、ありがとうございました!)

実装していく

シンタックスハイライトしたいので

npm install gatsby-transformer-remark gatsby-remark-prismjs prismjs

いろいろ表示させたいので

npm install gatsby-remark-embedder

github gistを表示させたいので

npm install gatsby-remark-embed-gist

mdxを使いたいので

npm install @mdx-js/mdx @mdx-js/react gatsby-plugin-mdx

embed-gistのスタイルシートがきかない問題

mdxにしたらいい感じにならないので外部CSSを手動で読み込むようにする

参考:Embed gist on gatsby-mdx page · Issue #12 · weirdpattern/gatsby-remark-embed-gist · GitHub

追加開発

  • 一覧・詳細に記事のタグ・カテゴリを表示
  • タグ・カテゴリ別記事一覧ページ
  • 一覧ページにページャー
  • Disqusの表示

一覧・詳細に記事のタグ・カテゴリを表示

postmetaコンポーネントを作って、一覧・詳細ともにこちらを呼び出すことにした。

import * as React from "react"
import PropTypes from "prop-types"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolder, faCalendar } from "@fortawesome/free-regular-svg-icons"
import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import {Link} from "gatsby";

const _ = require('lodash')
const PostMeta = ({ date, categories, tags }) => {
  return (
    <div className="post-meta">
      <div className="post-meta_item">
        <FontAwesomeIcon icon={faCalendar} />
        <time className="time" dateTime="{date}">{date}</time>
      </div>
      {categories.length > 0 && (
        <div className="post-meta_item meta-item-list">
          <ul className="categories">
            {categories.map((cat, i) => {
              const cat_uri = '/category/' + fixedEncodeURI(cat) + '/'
              return (
                <li key={i}><FontAwesomeIcon icon={faFolder} /><Link to={cat_uri} itemProp="url">{cat}</Link></li>
              )
            })}
          </ul>
        </div>
      )}
      {tags.length > 0 && (
        <div className="post-meta_item meta-item-list">
          <ul className="tags">
            {tags.map((tag, i) => {
              const tag_uri = '/tag/' + fixedEncodeURI(tag) + '/'
              return (<li key={i}><FontAwesomeIcon icon={faHashtag}/><Link to={tag_uri} itemProp="url">{tag}</Link></li>)
            })}
          </ul>
        </div>
      )}
    </div>
  )
}

function fixedEncodeURI(str) {
  return encodeURI(_.kebabCase(str).replace(/[.]/g, function(c) {
    return '%' + c.charCodeAt(0).toString(16);
  }));
}

PostMeta.defaultProps = {
  date: '',
  categories: [],
  tags: [],
}

PostMeta.propTypes = {
  date: PropTypes.string,
  categories: PropTypes.arrayOf(PropTypes.string),
  tags: PropTypes.arrayOf(PropTypes.string),
}

export default PostMeta

これをcomponentとして以下のようにデータ渡して呼び出し

<PostMeta date={post.frontmatter.date} categories={post.frontmatter.category} tags={post.frontmatter.tag} />

postやらfrontmatterやらでまるごと渡してもいいかな・・・と思ったけどこの方がわかりやすいかなって。

元々のブログでも使っていたので、fotawesomeを導入しています。
npm i -S @fortawesome/fontawesome-svg-core @fortawesome/react-fontawesome @fortawesome/free-regular-svg-icons @fortawesome/free-solid-svg-icons @fortawesome/free-brands-svg-icons

カテゴリ・タグの名前が日本語でエクスポートされる問題

タグやカテゴリはWP時代はslugを持っていてそちらをURLに使っていたのですが、今回は全部文字列そのまんまでエクスポートされているので encodeURIをかけてURLにすることにしたのですが、タグ名に「vue.js」とかURLとしてそのまま使うのが厄介な文字列がありまして、 解消するために「.」も変換するような関数を通してそこでURLがいい感じになるようにしています。
参考:encodeURIComponent を使ってエスケープされない文字もエンコードしたいとき

URL的に再現しきれない部分はetlifyのリダイレクトかけるかな〜という感じ。

タグ・カテゴリ別記事一覧ページ

templates以下にカテゴリ用テンプレート追加

import React from 'react'
import {graphql} from 'gatsby'
import Layout from '../components/layout'
import Seo from "../components/seo"
import Articles from "../components/articles" // 記事表示部分は一覧・カテゴリ/タグ別一覧で共通なのでコンポーネントに切り出し済み

const Category = ({ pageContext, data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const {category} = pageContext
  const posts = data.allMdx.nodes
  const title = "カテゴリー「" + category + "」の記事一覧"
  return (
    <Layout location={location} title={siteTitle}>
      <Seo title={title} />
      <h1 className="archive-title">{title}</h1>
      <Articles posts={posts} />
    </Layout>
  )
}

export default Category

export const pageQuery = graphql`    
    query($category: String) {
        site {
            siteMetadata {
                title
            }
        }
        allMdx(
            sort: { fields: [frontmatter___date], order: DESC },
            filter: { frontmatter: { 
                category: { eq: $category },
                status: {eq: "publish"}
            } }
        ) {
            nodes {
                excerpt
                slug
                frontmatter {
                    date(formatString: "YYYY/MM/DD")
                    title
                    category
                    tag
                }
            }
        }
    }
`

このテンプレートを使ってgatsby-nodeの中で一覧ページが作成されるように処理を追加します。

()
exports.createPages = async ({ graphql, actions, reporter }) => {
  ()
  const CategoryTmp = path.resolve(`./src/templates/category.js`)
  ()
  // 全記事データをカテゴリ・タグ含んで取得
  const result = await graphql(
      `
        query{
          allMdx(
            sort: { fields: [frontmatter___date], order: ASC },
            filter: {frontmatter: {status: {eq: "publish"}}}
          ) {
            nodes {
              id
              slug
              frontmatter {
                category
                tag
              }
            }
          }
        }
      `
    )
  ()
  const posts = result.data.allMdx.nodes
  ()
  let categories = []
    // ポストのカテゴリを一旦全取得
    _.each(posts, node => {
      if (_.get(node, 'frontmatter.category')) {
        categories.push(node.frontmatter.category[0])
      }
    })
    // lodashのuniqでユニーク化
    categories = _.uniq(categories)
    categories.map(category => {
      const uri = fixedEncodeURI(category)
      createPage({
        path: `/category/${_.kebabCase(uri)}/`,
        component: CategoryTmp,
        context: {
          category,
        },
      })
    })
}
()
function fixedEncodeURI(str) {
  return _.kebabCase(str).replace(/[.]/g, function(c) {
    return '%' + c.charCodeAt(0).toString(16);
  });
}

タグページも同様に生成。

一覧ページにページャー

gatsby-awesome-paginationを使う

gatsby-nodeではimportができなかったので以下の対応を行った gatsby-node.js doesn’t allow ES6 import · Issue #7810 · gatsbyjs/gatsby · GitHub

ページの説明通りに取得データにpaginateをかけるとpageContextにこんな感じのデータが入るので、これを利用してページャーを作成する

humanPageNumber: 4
limit: 10
nextPagePath: "/page/5"
numberOfPages: 6
pageNumber: 3
previousPagePath: "/page/3"
skip: 30

とってもシンプルなコンポーネントとして用意

import * as React from "react"
import PropTypes from "prop-types"
import { Link } from "gatsby";

const Pagination = ({ data }) => {
  return (
    <div className="pagination">
      {data.numberOfPages > 1 && (
      <p className="page-data">{data.humanPageNumber} / {data.numberOfPages}</p>
      )}
      <ul>
        {data.previousPagePath && (
          <li><Link to={data.previousPagePath}>前のページ</Link></li>
        )}
        {data.nextPagePath && (
          <li><Link to={data.nextPagePath}>次のページ</Link></li>
        )}
      </ul>

    </div>
  )
}

Pagination.defaultProps = {
  data: {}
}
Pagination.propTypes = {
  data: PropTypes.object,
}

export default Pagination

Disqusの表示

Disqusに登録してshortnameをゲットしたらあとはプラグインを利用します。 gatsby-plugin-disqus gatsby-configのpluginsに設定を追加する disqus_shortnameは環境変数に入れたのを読み込んでます。

{
  resolve: `gatsby-plugin-disqus`,
  options: {
    shortname: process.env.DISQUS_SHORTNAME
  }
}

呼び出したい箇所で以下のように呼び出す。

const disqusConfig = {
    url: post_url,
    identifier: post.id,
    title: post.frontmatter.title,
  }
<Disqus config={disqusConfig} />

ちなみに、exampleで紹介されている以下のような書き方だとidentifierがundefinedだよみたいなエラーが出るので上記のconfigで読み込む形にしました。

const PostTemplate = () => (
  <>
    /* Page Contents */
    <Disqus 
      identifier={post.id}
      title={post.title}
      url={`${config.siteUrl}${location.pathname}`}
    />
  </>
)

Github

Githubで何か特別に設定する必要はなく、リポジトリ作成→pushでOK プライベートリポジトリでもNetlifyで利用可能。

Netlify

Githubとの連携などは案内にそってやっていくと問題なく完了する。
GatsbyJS製のサイトね!と、Netlifyがいい感じのデプロイ設定をしてくれたのでデプロイ周りはこれといって追加でやったことはありません。

ドメインの接続

ドメイン(skylarking.me)は現在のロリポップサーバーに向いたまま、サブドメイン(blog.skylarkineg.me)はNetlifyを向いてほしい。

Netlifyでやること

[site setting][domain management]からadd custom domainで追加したいサブドメインを入力する。

ドメイン管理からやること

ドメインを管理しているムームードメインのコンパネからDNSをカスタム設定してサブドメインにCNAMEをあてがう。 ムームードメインの設定

これでドメイン設定完了。楽〜!これが無料で使えるなんて! ドメイン代のみで独自ドメインのサイトが持てると聞いてはいたが、スモールスタートにめちゃくちゃいいねと改めて感じました。

今後考える

コンテンツのこと

もし記事や画像がすごく増えたらソースに持つのはどうかなと思うのでやり方考えないといけないなぁ。 Contentful, netlifycms, MicroCMSなどを使ってコンテンツを分離するというのが一番無難なんだろうな。 ContentfulとMicroCMSは少し触ってみたけど、コンテンツとして持てる本文の自由度の低さが少し気になってしまった。

Netlify DNS

今回はサブドメインのみNetlifyに向ける構成なのでNetlify DNSを使うことは諦めたけど Netlify CDN使えたら嬉しいので構成考えてみようかしら


Profile picture

ぴーやま
プログラミングを嗜んでします。中華料理で出てくるたまごふわふわのコーンスープが好きです。