Next.jsでブログを構築する〜環境構築から表示まで 2021.11.18
背景
以前のブログをMiddlemanというRuby製のサイトジェネレータで構築しており、単純にbuildディレクトリの成果物を静的サイトとしてNetlifyに移行するつもりだった。
このブログでも便利に使っているGatsbyのブラックボックスの部分が垣間見えるかもと思い、Next.jsでのブログ構築の材料にしてみた。
環境構築
% npx create-next-app@latest
% cd 110chang-com-next
% yarn dev
yarn run v1.22.11
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
event - compiled successfully
markdownファイルを読み込めるようにする
ブログと言えば、まずMarkdownファイルを読み込めないと始まらない。
参考に公式のブログスターターを見る。
To create the blog posts we use remark and remark-html to convert the Markdown files into an HTML string, and then send it down as a prop to the page. The metadata of every post is handled by gray-matter and also sent in props to the page.
lib/api.jsでmarkdownを取得している。
// lib/api.js
import fs from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
const postsDirectory = join(process.cwd(), '_posts')
export function getPostSlugs() {
return fs.readdirSync(postsDirectory)
}
export function getPostBySlug(slug, fields = []) {
const realSlug = slug.replace(/\.md$/, '')
const fullPath = join(postsDirectory, `${realSlug}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
const items = {}
// Ensure only the minimal needed data is exposed
fields.forEach((field) => {
if (field === 'slug') {
items[field] = realSlug
}
if (field === 'content') {
items[field] = content
}
if (typeof data[field] !== 'undefined') {
items[field] = data[field]
}
})
return items
}
export function getAllPosts(fields = []) {
const slugs = getPostSlugs()
const posts = slugs
.map((slug) => getPostBySlug(slug, fields))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
return posts
}
lib/markdownToHtml.jsでmarkdownをパースしている。
// lib/markdownToHtml.js
import remark from 'remark'
import html from 'remark-html'
export default async function markdownToHtml(markdown) {
const result = await remark().use(html).process(markdown)
return result.toString()
}
必要そうなパッケージを追加。
% yarn add remark remark-html gray-matter
postsディレクトリを追加して適当なmarkdownファイルを追加。
移行元のfrontmatterは以下のような形式になっている。とりあえず、title, dateあたりを読み込めるようにする。slugはファイル名から。contentsはfrontmatter以降の部分。
---
title: Title
date: 2021-01-02T03:45:00+09:00
tags: [Tag1, Tag2]
categories: [Category]
aliases:
- /diary/aliases
---
最低限のトップページのビュー。
// pages/index.js
// ...
export default function Home({ allPosts }) {
return (
<div>
{/* ... */}
<main className={styles.main}>
{allPosts.length > 0 && allPosts.map((post) => (
<div key={post.date}>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
))}
</main>
</div>
)
}
export async function getStaticProps() {
const allPosts = getAllPosts([
'title',
'date',
'slug',
'content',
])
return {
props: { allPosts },
}
}
dateはそのままだとパースできないので、いったん無理やり文字列に変えて通す。あとでday.jsの処理を入れる。
// lib/api.js
export function getPostBySlug(slug, fields = []) {
// ...
items.date = items.date.toLocaleString()
return items
}
記事ページを追加
posts/[slug].js
を追加。
markdownToHtml関数を使うとエラー。
error - lib/markdownToHtml.js (5:29) @ markdownToHtml
TypeError: (0 , remark__WEBPACK_IMPORTED_MODULE_0__.default) is not a function
remark
のインポートの仕方がnamed importに変わっている。remark-htmlのほうはdefault import。
import { remark } from 'remark'
import html from 'remark-html'
画像がレンダリングされない。sanitizeオプションを指定。
// lib/markdownToHtml.js
import { remark } from 'remark'
import html from 'remark-html'
export default async function markdownToHtml(markdown) {
const result = await remark().use(html, { sanitize: false }).process(markdown)
return result.toString()
}
デザイン
もとのブログからデザインを持ってくる。SCSSとして追加したかったが、compassというRuby Gemを使っているので今回はビルド済みのCSSを直接持ってくることにした。
compassのサイトはSSL化もされていない。諸行無常。
% cp build/css/com.css ../110chang-com-next/styles
画像も持ってくる。
% cp -r source/img/ ../110chang-com-next/assets
CSS内の画像へのパスを調整。
.sprite-icons {
- background: transparent url(../img/sprite-icons.png) no-repeat 0 0;
+ background: transparent url(../assets/sprite-icons.png) no-repeat 0 0;
}
ルーティング
/[category]/[slug]
にしたいが、まずは/posts/[category]/[slug]
を実現する。
getStaticPaths
ページが動的ルート(ドキュメント)を持ち、getStaticPropsを使用する場合、ビルド時に HTML をレンダリングするためのパス一覧を定義する必要があります。
- ディレクトリ構造を
posts/[category]/[slug].js
とする。 [slug].js
のgetStaticPathでparams
にcategory
とslug
を返す。
カテゴリーが複数型になっているのはHugoを試したときの名残。
// [slug].js
// ...
export async function getStaticPaths() {
const posts = getAllPosts(['slug', 'categories'])
return {
paths: posts.map((post) => {
const category = post.categories ? post.categories[0] : 'diary'
return {
params: {
slug: post.slug,
category,
},
}
}),
fallback: false,
}
}
以前のパスからリダイレクトをかけておく。
// next.config.js
// ...
module.exports = {
// ...
async redirects() {
return [
{
source: '/:category(diary|knowledge|...)/:path*',
destination: '/posts/:category/:path*',
permanent: true,
},
]
},
}