ブログを Next.js に移行した
つい先日、このブログを Gatsby から Next.js に移行した。
このブログを開発し始めたのは2019年末のことで、実は最初は Next.js を採用しようと目論んでいた。 しかし React や Vue での開発経験がほとんどなかった当時の自分にはハードルが高く、より簡単に始められそうな Gatsby を選択したのだった。
そこから月日は流れて2021年9月のシルバーウィーク、ある程度のフロントエンドの経験を積んだ自負もあったのに加え、知らぬ間に Next.js が SSG サポートしていたこともあり、もう一度チャレンジしてみた。
Next.js は公式から Gatsby から Next.js への移行ガイドが提供されている。 しかし今回は移行に加えて
- TypeScript の導入
- Tailwind CSS の導入
- ページ階層の整理
など、併せて実施したい作業も多かった。そのため Create Next App から始めて一から作り直すことにした。
プロジェクトの雛形の作成
始めに Create Next App でプロジェクトの雛形を作成する [^1] 。 今回は以下のテンプレートを利用した。
next.js/examples/blog-starter-typescript at master · vercel/next.js
tmp
以下に生成したファイルを現ディレクトリに移動し、 tmp
は削除する。
yarn create next-app --example blog-starter-typescript tmp
Linter と Formatter の設定
今回は以下のツールを導入した。
まず必要なパッケージを追加する。
# ESLint 関連のパッケージ
yarn add --dev eslint eslint-config-next @typescript-eslint/eslint-plugin @typescript-eslint/parser
# Stylelint 関連のパッケージ
yarn add --dev stylelint stylelint-scss stylelint-config-recommended-scss
# Prettier 関連のパッケージ
yarn add --dev --exact prettier
yarn add --dev eslint-config-prettier stylelint-config-prettier
ESLint の設定は以下の通り。
.eslintrc.json
では ESLint と Prettier のルールの競合を防ぐための設定 ("prettier"
) を継承している [^2] 。
.eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"prettier"
]
}
.eslintignore
**/node_modules/*
**/.next/*
Stylelint の設定は以下の通り。
Tailwind CSS のシンタックスがエラーになるのを避けるために "ignoreAtRules"
に幾つかルールを追加している [^3] 。
.stylelintrc.json
{
"extends": [
"stylelint-config-recommended-scss",
"stylelint-config-prettier"
],
"rules": {
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"variants",
"responsive",
"screen"
]
}
]
}
}
Prettier の設定は以下の通り。
.prettierrc.json
{
"semi": false,
"singleQuote": true
}
.prettierignore
node_modules
.next
yarn.lock
public
次に pre-commit hook を設定する。 README に従って以下のスクリプトを実行する。
npx mrm@2 lint-staged
スクリプト実行後に package.json
に設定が追加されるので、それを上書きする [^4] 。
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,sass,scss}": "stylelint --fix",
"*.{js,jsx,ts,tsx,html,css,sass,scss,md}": "prettier --write"
}
}
CSS を Sass に置き換え
Next 11.0.0 では Sass を組み込みでサポートしている。
まず sass
パッケージを追加する
yarn add sass
後は拡張子が .css
になっているファイルの名前を .scss
に変更すれば良い。
テンプレートの修正
テンプレートのままだと不都合ないくつかの点について修正した。
Favicon の差し替え
以前は Web 上の Favicon 生成ツールで作った没個性的な画像を使っていたので、今回は自分で描いてみた。 パートナーの趣味の影響 [^5] で少し上手く描けるようになっていたペンギンを採用した。
各種 Favicon 画像ファイルの生成には RealFaviconGenerator を利用した。
Twitter カード用の meta タグを追加
すべてのページに適用する meta.tsx
には以下のように Twitter カード用の meta タグを追加した。
components/meta.tsx
const Meta: React.FC = () => {
const description = 'some description'
return (
<Head>
<!-- ... -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@twitter_id" />
<meta name="twitter:title" content="Blog Title" />
<meta name="twitter:description" content="This is blog descriptions." />
<meta name="twitter:image" content="/assets/cover.png" />
</Head>
)
}
ブログの記事詳細ページに当たる pages/posts/[slug].tsx
にも追加し、記事詳細の内容で meta タグを上書きするようにした。
og:image
タグの重複排除
デフォルトの og:image
とページ固有の og:image
の両方がレンダリングされてしまう問題があった。
key
を指定して一度だけレンダリングされることを保証するようにした。
<meta property="og:image" content="/assets/cover.png" key="og:image" />
箇条書きやリンクなどのスタイルを調整
記事本文中のスタイルを微修正した。
components/markdown-styles.module.scss
.markdown li {
@apply my-1 ml-8;
ul, ol {
@apply my-2;
}
}
.markdown ul > li {
@apply list-disc;
}
.markdown a {
@apply text-blue-400 hover:underline;
}
MDX の導入とカスタムコンポーネントの追加
旧ブログでは MDX を導入して Markdown 中で React コンポーネントを呼び出せるようにしていた。 新ブログでも引き続きその需要があるので、 next-mdx-remote [^6] を利用して MDX を導入した。
まず必要なパッケージを追加する。 今回はコードブロックの syntax highlight に PrismJS を利用したかったため、併せて remark-prism を追加した。
yarn add next-mdx-remote prismjs remark-prism
記事ファイルの拡張子を .mdx
に変更し、 lib/api.ts
で読み込み先ファイルの拡張子の指定を変更する。
export function getPostBySlug(slug: string, fields: string[] = []): Items {
const realSlug = slug.replace(/\.mdx$/, '')
const fullPath = join(postsDirectory, `${realSlug}.mdx`)
// ...
}
記事ファイルから読み込んだ文字列を HTML に変換してコンポーネントに渡している箇所があるので、それらを修正する。
以下では例として、 <YoutubeVideo>
という Youtube 動画の iframe の幅をレスポンシブに調整するためのコンポーネントを MDX ファイル内に記述すると、 React コンポーネントとしてレンダリングされるように設定している。
また next-mdx-remote では文字列を一度 serialize する工程があり、そのオプションに remark-prism を指定することで PrismJS で syntax highlight できる形式にレンダリングすることができる。
components/youtube-video.tsx
const YoutubeVideo: React.FC<Props> = ({ url }: Props) => {
return (
<div className="relative w-full h-0 pb-[56.25%]">
<iframe
src={url}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
className="absolute top-0 left-0 w-full h-full"
/>
</div>
)
}
components/post-body.tsx
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
import YoutubeVideo from './youtube-video'
type Props = {
source: MDXRemoteSerializeResult
}
const components = { YoutubeVideo }
const PostBody: React.FC<Props> = ({ source }: Props) => {
return (
<div className="max-w-2xl mx-auto">
<div className={markdownStyles['markdown']}>
<MDXRemote {...source} components={components} />
</div>
</div>
)
}
pages/posts/[slug].tsx
import { MDXRemoteSerializeResult } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize'
type PostProps = {
post: Items
source: MDXRemoteSerializeResult
}
const Post: React.FC<Props> = ({ post, source, preview }: Props) => {
// ...
return (
<Layout preview={preview}>
<Container>
<Header />
<>
<article className="mb-32">
<!-- ... -->
<PostBody source={source} />
</article>
</>
)}
</Container>
</Layout>
)
}
export async function getStaticProps({
params,
}: Params): Promise<GetStaticPropsResult<PostProps>> {
const post = getPostBySlug(params.slug, [
// ...
'content',
])
const mdxSource = await serialize(
post.content,
{
mdxOptions: {
remarkPlugins: [
[
require('remark-prism')
]
]
}
}
)
return {
props: {
post: {
...post,
},
source: mdxSource
},
}
}
また PrismJS 用の CSS は pages/_app.tsx
で import して、グローバル CSS として適用しておく。
import 'prismjs/themes/prism.css'
Google Tag Manager
Google Tag Manager のスクリプトの設置には react-gtm-module を利用した。
pages/_app.tsx
import TagManager from 'react-gtm-module'
export default function MyApp({
Component,
pageProps,
}: AppProps): ReactElement {
useEffect(() => {
TagManager.initialize({ gtmId: 'GTM-XXXXXXX' })
}, [])
return <Component {...pageProps} />
}
また Google Tag Manager の設定で、ページビューのトリガーでは戻るボタンでページ移動したときのイベントを補足できなかったので、 History Change をトリガーに加えて対処した [^8] 。
デプロイ
元々は Netlify でホスティングしていたが、日本国内からのアクセスなら Vercel の方が早いという話もあり、せっかくなので Vercel に鞍替えすることにした。
Vercel へのデプロイは該当の GitHub リポジトリを選択してビルド設定を追加するだけで完了する。
ネームサーバーに関しては、以前は DNS の設定上の都合で Netlify のネームサーバーを利用していたが、 Vercel の場合は特に不都合がないのでドメイン管理会社のネームサーバーに移管した [^7] 。 その後 Vercel の Domains 設定ページで対象のドメインを追加し、表示されるレコードをネームサーバーに追加する。 追加が完了して少し待つと Vercel の Domains 設定ページの表示が "Valid Configuration" に変わるので、それを確認する。 また少し待つとサイトが表示できるようになる。
まとめ
Next.js のテンプレートから始めてブログを公開するまでの手順をまとめた。
細かい部分を調べつつ、ブログも書きつつ作業した結果、最終的な作業時間は30時間を超えていた。 とは言え、こだわらなければあまり時間を掛けずに SSG な爆速ブログを開発できるので、 Next.js はオススメ。
[^1]: 途中まで Docker でローカル開発環境を作っていたが、 IDE や Vim 上で Linter を機能させるだけで消耗しそうだったので、今回は断念した。
[^2]: ESLint と Prettier の共存のための設定方法については、本記事の執筆時点では eslint-config-prettier で Prettier の不要なルールを OFF にするのが推奨されるらしい。他には @sadnessOjisan さんの記事や @soarflat さんの記事を参考にした。
[^3]: 基本的には at-rule-no-unknown
に例外を追加するのだが、 stylelint-config-recommended-scss
を導入した場合は at-rule-no-unknown
が無効化され、 scss/at-rule-no-unknown
が有効になっているので注意する。
[^4]: Next 11.0.0 で ESLint が統合されたため、 next lint
で lint を実行することも可能。しかし pre-commit hook の実行時にエラーとなってしまったため、今回は使用を見送っている。
[^5]: パートナーがペンギンの LINE スタンプを販売しているので、是非購入を検討してほしい。
[^6]: 他にも幾つかの導入手段があり、 Tyler Smith さんのライブラリ比較記事を参考に選出した。
[^7]: 自分はうっかり元のネームサーバーの TTL を 3600 にしたまま移管したので、通信経路によってはしばらくブログにアクセスできない状態だった。