GatsbyからNext.jsへのサイト移行

GatsbyからNext.jsへのサイト移行

2022-04-0433 min read

目次

  1. 概要
  2. 移行の目的
  3. 移行作業の工程
  4. nextjsでプロトタイプ版のサイト構築
  5. コンテンツの変換項目洗い出し変換移動
  6. adsenseanalyticssitemapogp等の設定
  7. 試験環境本番環境構築
  8. 検討したけどやらなかったこと

概要

このブログを Gatsby から Next.js に移行しました。

GatsbyからNextへの移行方法は Migrating from Gatsby に基本的には記載されていますが、 細かい設定やプラグイン周りについては各々で調査して移行方法を検討する部分があると思われるので、その部分について記載しました。

移行の目的

特にGatsbyに不満があった訳ではありません。 Gatsbyを1~2年利用しましたが、とても優れたツールだと思っています。

それにも関わらずNextに移行した理由は、

  • 単純にNextの勉強をしたかった
  • プラグインとして隠蔽されてしまっている部分を自分で実装してみたくなった
  • Vercel が開発している安定感
  • 情報量も十分

といったところです。

移行作業の工程

作業工程について、次のようなタスクをリストアップし工程を組んで実施しました。

  1. Next.jsでプロトタイプ版のサイト構築 (3d)
  2. コンテンツの変換項目洗い出し・変換・移動 (1d)
  3. Adsense・Analytics・Sitemap・OGP等の設定 (2d)
  4. 試験環境・本番環境構築 (1d)
  5. デプロイの検証 (0.5d)
  6. 本番化 (0.5d)

※ 括弧内は実際に費やした時間です。1d = 8hとしています。

Next.jsでプロトタイプ版のサイト構築

移行前は次の様な技術スタックで構築していました。

  • Gatsby + React(js) でガワを実装
  • コンテンツはmarkdownファイルでGit管理(ヘッドレスではない)
  • GitHub Actions でCI/CDを構築
  • Amazon S3 + CloudFront でコンテンツ配信

移行後は次のようにしました。

  • Next.js + React + TypeScript + TailwindCSS
    • SSG方式を採用
  • コンテンツはmarkdownファイルでGit管理(ヘッドレスではない)
  • GitHub Actions でCI/CDを構築
  • Amazon S3 + CloudFront でコンテンツ配信

つまりガワの部分だけが主な変更となります。

ディレクトリ構成

NextJSでmarkdownファイルを扱う

NextJSでMarkdownファイルを表示する大まかな仕組みについて、基本的な実装はこちらを参考にしました。

Next.js で Markdown ブログを作る

こちらの記事の実装では単純な記事の羅列・表示を行なっていますが、タグ・年月ごとのソート・集計を行いたかったので、 データの取得部分の実装をclassで定義してにしてタグごとの集計データを返却するメソッドを追加しました。(utils/posts-manager.ts)

Markdownのパース

ファイルをmarkdownからHTMLへ変換する部分については remark, rehype, unified, grey-matter 等のパッケージ及びそれらのプラグインを利用して変換を行なっています。

この部分については Remark・Rehype プラグインで文書の見出しに自動で ID を振り目次リストを自動生成する を参考にしました。

(utils/md.ts)

シンタックスハイライト

シンタックスハイライトにはshikiを利用しました。

実装

実装は以下となります。

※いくつか端折っています。

utils/posts-manager.ts

import fs from 'fs'
import { join } from "path";
import matter from "gray-matter";
import {
  Posts,
  IGroupByItems,
} from "types/entry.interface"
import getConfig from "next/config";

export interface Posts {
  title: string;
  path: string;
  date: string;
  coverImage: string;
  tags: string[];
  filepath: string;
};

export interface IGroupByItems {
  name: string;
  counts: number;
}

const { publicRuntimeConfig } = getConfig(); //後述

const listFiles = (dir: string): string[] =>
  fs.readdirSync(dir, { withFileTypes: true }).flatMap(dirent =>
    dirent.isFile() ? [`${dir}/${dirent.name}`] : listFiles(`${dir}/${dirent.name}`)
  )

class PostsManager {
  /**
   * 付属情報格納
   */
  private data: Array<Posts>
  /**
   * タグごとにソートした記事
   */
  private dataGroupByTag: Map<string, Posts[]>;
  // 利用しているタグ名
  private tagNames:  string[] = []

  /**
   * 
   * @param basePath 
   */
  constructor(basePath: string) {
    const files = listFiles(basePath)
    const result = []
    for (let i = 0; i < files.length; i++) {
      if (!files[i].endsWith("/index.md")) {
        continue
      }
      const fileContents = fs.readFileSync(files[i], "utf8");
      const { data } = matter(fileContents);
      data.filepath = files[i]
      result.push(<Posts>data)
    }
    const dataGroupByTag = new Map<string, Posts[]>()
    this.data = result.map(post => {
      //
      // tag集計
      //
      post.tags = Array.from(new Set(post.tags))
      this.tagNames = Array.from(new Set(this.tagNames.concat(post.tags)))
      post.tags.forEach(tag => {
        let tmp = dataGroupByTag.get(tag)
        if (!tmp) {
          tmp = []
        }
        tmp.unshift(post)
        dataGroupByTag.set(tag, tmp)
      })
      return post
    })
    this.dataGroupByTag = dataGroupByTag
    // path:/entry/${id} でソート
    this.data = this.data.sort((a: Posts, b: Posts): number => {
      let ai = Number(a.path.split('/').pop())
      let bi = Number(b.path.split('/').pop())
      return bi - ai
    })
  }

  /**
   * 
   * @returns 
   */
  public getData() {
    return this.data
  }

  /**
   * 
   * @param path 
   * @returns 
   */
  public findByPath(path: string): Posts | undefined {
    const data = this.data
    for (let i = 0; i < data.length; i++) {
      const row = data[i]
      if (path === row.path) {
        return row
      }
    }
    return
  }

  /**
   * タグで一覧検索
   * @param tag 
   * @returns 
   */
  public findByTag(tag: string): Posts[] {
    const data = this.getAllGroupByTags().get(tag)
    if (!data) {
      return []
    }
    return  data
  }

  /**
   * tagでソートして取得
   * @returns 
   */
  public getAllGroupByTags(): Map<string, Posts[]> {
    return this.dataGroupByTag
  }

  /**
   * tagでソートして取得
   * @returns 
   */
  public getCountsGroupByTags(sort: 'desc' | 'asc' = 'desc'): IGroupByItems[] {
    const tagNames = this.getAllTagNames()
    const tagsCounts = []
    for (let i = 0; i < tagNames.length; i++) {
      const tag = tagNames[i]
      tagsCounts.push({
        name: tag,
        counts: this.findByTag(tag).length
      })
    }
    return tagsCounts.sort((prev, next) => {
      if (sort === 'asc') {
        return prev.counts - next.counts
      }
      return next.counts - prev.counts
    })
  }
}

// postsが格納されているディレクトリを取得する
const postsDirectory = join(process.cwd(), "content/posts");

export default new PostsManager(postsDirectory)

utils/md.ts

import fs from "fs";
import matter from "gray-matter";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeShiki from "@leafac/rehype-shiki";
import * as shiki from "shiki";
import remarkGfm from "remark-gfm";
import remarkHtml from "remark-html";
import remarkSlug from "remark-slug";

interface Props {
  filepath: string;
}

const markdownToHtml = async (opt: Props) => {
  const fileContents = fs.readFileSync(opt.filepath, "utf8");
  const { data, content } = matter(fileContents);
  const result = await unified()
    .use(remarkParse)
    .use(remarkHtml)
    .use(remarkSlug)
    .use(remarkGfm)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeShiki, {
      highlighter: await shiki.getHighlighter({
	      theme: 'github-dark',
      }),
    })
    .processSync(content);
  return result.toString()
};

呼び出し側の例

import PostsManager from 'utils/posts-manager';
import markdownToHtml from 'utils/md';

export const getStaticPaths = async () => {
  const posts = PostsManager.getData()
  const result = {
    paths: posts.map((post: any) => {
      return {
        params: {
          id: post.path.split('/').pop(),
        },
      };
    }),
    fallback: false,
  };
  return result
};

export const getStaticProps = async ({ params }: any) => {
  const tags = PostsManager.getCountsGroupByTags().slice(0, 50)
  const post = PostsManager.findByPath(`/entry/${params.id}`)
  const contentObj = await markdownToHtml({
    filepath: post.filepath
  });
  return {
    props: {
      post: {
        ...post,
        content: contentObj.html,
      },
      tags,
    },
  };
};

コンテンツの変換項目洗い出し・変換・移動

記事のパスの調整

Gatsbyを利用していた際は、 各記事のmarkdownファイルのメタデータ領域にファイルパスを定義していました。

content/entry/300/271/index.mdファイルの場合

---
title: "タイトル..."
path: "/entry/271"
date: "2022-04-03 01:00"
tags: ["javascript", "typescript"]
---

※上記のメタデータの場合、https://ドメイン/entry/271 としてHTMLファイルを吐き出します

content/entry/300/271/index.mdの"300"の意図はファイルを1つのディレクトリに数百個コンテンツが存在すると視認性が悪くなるので、100記事単位で区切って管理していました。

このメタデータのパスを利用したかったことと、コンテンツの構成を弄ることは避けたかったので、pages/entry/[id].tsxにファイルを置き、記事のIDにマッチするファイルを取得するようにパスの部分の実装を調整しました。

画像パスの調整

記事内の画像ファイルはこれまでは画像ファイルと同じディレクトリに格納していました。

content/entry/300/271/index.md
content/entry/300/271/image1.png
content/entry/300/271/image2.jpg

また記事内では相対パスと絶対パスが混在している状態となってました。

## Article Head

hello world

![](./image1.png)

![](/entry/image2.jpg)

これについては、画像ファイルをpublicディレクトリに移動することで解決しました。

content/entry/300/271/index.md
public/entry/271/image1.png
public/entry/271/image2.jpg

Nextのビルド時に画像をdataURLに変換して記事に埋め込む方法も検討しましたが、 難易度が高かったので辞めました。

Adsense・Analytics・Sitemap・OGP等の設定

Google Adsense

Adsense周りの実装はこちらを参考にしました。

[Next.js] Google Adsenseを表示させる方法

Google Analytics

Analytics周りの実装はこちらを参考にしました。

Next.jsでGoogle Analyticsを使えるようにする

Chromeの拡張機能であるGoogle Analytics Debuggerを利用することでローカル環境でもタグ周りの動作を確認できるので便利でした。

Sitemap

Sitemapはこちらを参考にしました。

Next.js に next-sitemap を導入して超手軽にサイトマップ sitemap.xml を生成しよう

OGP

メタタグを設定することで問題なく設定できました。

metaタグはnext/headを用いることで利用できます。

実装例

import Head from 'next/head'

const Ogp = (props) => {
  return (
    <Head>
      <title>{props.title}</title>
      <meta name="viewport" content="width=device-width,initial-scale=1.0" />
      <meta name="description" content={props.description} />
      <meta property="og:url" content={props.url} />
      <meta property="og:title" content={props.title} />
      <meta property="og:site_name" content={props.title} />
      <meta property="og:description" content={props.description} />
      <meta property="og:type" content="website" />
      <meta property="og:image" content={props.imgUrl} />
      <link rel="canonical" href={props.url} />
    </Head>
  )
}

twitter用のogpも同様に設定できました。

試験環境・本番環境構築

CI/CD

コンテンツのホスティングにはAmazon S3 と cloudfrontを利用しています。

これはGatsby環境から引き続き同じ環境にデプロイします。

CI/CDにはGitHub Actionsを利用しています。

↓はGatsby時の設定ですが、ほとんど同じ構成でNextもビルド〜デプロイしています。

GitHub Actions で Gatsby をビルドし Amazon S3 にデプロイする

サブディレクトリの利用

通常の検証では検証用の環境を利用しますが、 本番のドメインで検証したいことがあり、本番環境のサブディレクトリにコンテンツを配置して検証しました。

Nextでサブディレクトリを利用したい場合は次の様に設定を行うことでサブディレクトリで利用できます。

const basePath = '/subdir'

const nextConfig = {
  basePath, // アプリケーションのパスprefix
  publicRuntimeConfig: {
    basePath
  }
}
module.exports = nextConfig

publicRuntimeConfigはSSR,SSGでも利用できるランタイムの設定を定義できる領域です。 サブディレクトリのパスを定義しておき、各種ファイルのパスを調整するのに利用しました。

import getConfig from 'next/config';

const { publicRuntimeConfig } = getConfig();
const { basePath } = publicRuntimeConfig

検討したけどやらなかったこと

以下のものは検討したけど利用しなかったものです。

  • SSR

更新頻度が低いのでSSGで十分だと判断しました。

  • MDX

ただのドキュメント作成・管理にJSXを利用するメリットは大きくないと思いました。 また、別のツールに乗り換えることを考えた時に大きな負債になる可能性があると思ったからです。

  • 画像最適化

next/imageの利用及びその代替実装は行いませんでした。 そもそもnext/imageはSSGでは利用できませんでした。 next-optimized-imagesの導入も手こずったため、画像はimgタグを利用しました。

  • Chakra, MUI, 他UIフレームワーク (vs Tailwind)

何となく...

Recommends
GatsbyからNext.jsへのサイト移行
2022-04-04
next.js
gatsby
amazon%20aws
NextJSでDevToysのようなものを作成した
2022-02-22
javascript
typescript
vercel
[Next.js] Warning: Assign arrow function to a...
2022-02-13
javascript
typescript
next.js
AWS Amplify に Next.js (SSG) で作ったアプリをデプロイする
2021-01-07
javascript
react
next.js
Vue/Nuxt.js 触ってた人が Next.js に入門する
2021-01-03
javascript
react
next.js
AWS Amplify で monorepo を導入し 単一リポジトリで複数プロジェクトを...
2022-02-25
git
github
amazon%20aws
NestJSアプリケーションをwebpackでBundle
2022-02-20
javascript
typescript
nestjs
GitHub Actions で Gatsby をビルドし Amazon S3 にデプロイ...
2020-05-27
amazon%20aws
amazon%20s3
github
WordPressやめます Gatsbyに移行しました
2020-05-11
wordpress
blog
gatsby
[NestJS]少し大きな規模のRESTfull APIを構築するディレクトリ構成を考えて...
2022-09-04
nestjs
typescript
%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3
[AWS CDK] Cognito の OIDC プロバイダに Auth0 を設定
2022-07-03
auth0
amazon%20aws
aws%20cdk
Amazon S3 でライフサイクルポリシーを設定する
2022-06-19
amazon%20aws
amazon%20s3
Fisher-Yates shuffleで配列シャッフル [js/ts/php]
2022-06-19
javascript
node.js
typescript
[AWS CDK]ECS FargateでNestJSで作成したRESTfull APIデ...
2022-05-24
nestjs
amazon%20aws
aws%20cdk
[AWS CDK]S3 CloudFront OAI Route53 構成 で NextJ...
2022-05-23
amazon%20aws
aws%20cdk
typescript
New Posts
[JS]Intl.DateTimeFormatで和暦と西暦を変換
2022-08-18
javascript
[NestJS]少し大きな規模のRESTfull APIを構築するディレクトリ構成を考えて...
2022-09-04
nestjs
typescript
%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3
Prisma MySQL でUTC以外の任意のタイムゾーンを利用するのが難しい件
2022-08-08
prisma
typescript
mysql
Prisma TypeScript MySQLなプロジェクトの構築
2022-08-08
prisma
typescript
mysql
Prisma TypeScript SQLiteなプロジェクトの構築
2022-08-06
prisma
typescript
sqlite
[AWS]Lambda vs Fargate. APIを実装する場合に思うこと
2022-07-30
amazon%20aws
amazon%20ecs
%E9%9B%91%E8%AB%87
macOSにzigをインストールしてHello World!する
2022-07-18
zig
mac
[AWS CDK] Cognito の OIDC プロバイダに Auth0 を設定
2022-07-03
auth0
amazon%20aws
aws%20cdk
Amazon S3 でライフサイクルポリシーを設定する
2022-06-19
amazon%20aws
amazon%20s3
AWS Certified Developer Associate に合格した
2022-06-19
amazon%20aws
%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93
Fisher-Yates shuffleで配列シャッフル [js/ts/php]
2022-06-19
javascript
node.js
typescript
JavaScriptでUTF-16コードを文字列に変換
2022-06-18
javascript
node.js
[JS]乱数でランダムな整数を生成する
2022-06-18
javascript
node.js
[JS]BigIntでMathが使えない件
2022-06-12
javascript
node.js
atcoder
AWS SAPに合格しました
2022-06-11
amazon%20aws
%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93
Hot posts!
Proxy環境下でcurlを実行する
2019-12-07
linux
curl
OpenCVのMatのタイプ一覧表
2018-11-25
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86
opencv
Macでも利用できるDBクライアント MySQL PostgreSQL Oracle など
2019-12-21
linux
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9
mysql
TablePlusを使ってみる。シンプルでモダンなSQLクライアントツール
2018-09-30
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9
DBクライアントツールはDBeaverをおすすめしたい
2021-03-08
oracle
mysql
sqlite
AWS S3のアクセスキーIDとシークレットアクセスキーの取得 作業用ユーザを作成
2019-06-12
amazon%20aws
linux
amazon%20s3
AtCoderで初めて色がつくまでの話(茶色) レートが中々上がらなかった原因
2018-11-25
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0
%E9%9B%91%E8%AB%87
CentOS8でEPELとPowerToolsリポジトリの有効化
2020-11-30
centos
red%20hat
EPEL
Macでターミナルからポートスキャンを行う方法。
2018-12-09
linux
mac
apple
Python + OpenCVのfillConvexPolyで複雑なポリゴンを描画する
2018-11-27
python
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86
opencv
Date
▶︎
2022 年 (39)
▶︎
2021 年 (40)
▶︎
2020 年 (30)
▶︎
2019 年 (90)
▶︎
2018 年 (89)
▶︎
2017 年 (1)
Tags
javascript(98)
amazon%20aws(47)
linux(47)
node.js(38)
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0(36)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86(30)
html5(29)
typescript(28)
php(24)
centos(24)
python(22)
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0(21)
mysql(19)
mac(19)
canvas(18)
opencv(17)
%E9%9B%91%E8%AB%87(16)
wordpress(15)
atcoder(14)
docker(14)
apache(12)
%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92(12)
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9(12)
amazon%20s3(12)
red%20hat(12)
ubuntu(11)
github(10)
git(10)
vue.js(10)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86100%E6%9C%AC%E3%83%8E%E3%83%83%E3%82%AF(10)
mariadb(10)
aws%20cdk(9)
css3(8)
%E5%8F%AF%E8%A6%96%E5%8C%96(8)
%E5%B0%8F%E3%83%8D%E3%82%BF(8)
amazon%20lightsail(7)
react(7)
%E3%83%96%E3%83%AD%E3%82%B0(6)
cms(6)
oracle(6)
perl(6)
gitlab(6)
next.js(6)
iam(5)
amazon%20ec2(5)
%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93(5)
aws%20amplify(5)
curl(4)
webassembly(4)
ssh(4)
Author
s-yoshiki
s-yoshiki
githubzenntwitterqiita
ただの備忘録です。
JavaScript/TypeScript/node.js/React/AWS/OpenCV
※このブログの内容は個人の見解であり、所属する組織等の見解ではありません。