Next.jsのApp RouterでSSGしてブログを作る その2
前回 Next.js でブログを作るにあたって調べたことを記事にしたけど、その後も作っていたら色々調整が必要なところがあったので、それもメモしておこうと思う
記事内の画像が表示できない
僕のブログは以下のように記事のデータを保持している
1 つの記事とその記事に使う画像データを同一ディレクトリで持っているという感じ
以前駆け出しの時にブログ作った際、記事データも画像データも 1 つのディレクトリで管理していたら大変なことになったので、分けるようにした
./posts
├── yyyy
│ ├── mm
│ │ └── :slug
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── index.md
Gatsby.js を使っていた時は、記事の Markdown 内では ![1](./1.png)
みたいに書いていればちゃんと表示してくれたが、Next.js ではどうやらちゃんと /public
に置かないといけないらしい
今更 Markdown 内のリンク書き換え + 画像だけ /public
へ移動など絶対にしたくなかったので、ビルド時に以下のような処理を入れて回避することにした
posts/**/*.png
に該当する画像ファイルを/public/post-images/yyyy/mm/:slug/*.png
にコピーするようなスクリプトを用意- 1 のスクリプトを npm script に
prebuild
として登録(こうするとnpm run build
した時に自動的に処理してくれる) remark
のプラグインを自分で書いて、Markdown 変換処理の途中で記事内の画像の URL を/public
以下の正しいパスとなるように書き換える
ネットを検索すると同じようなことで悩んでいる人が多くて、色々記事とか対応方法がヒットしたけど、なるべくパッケージ入れたくないのと、remark
のプラグインの勉強になるので色々自前でやることにした
画像コピーの処理は glob
使ってガガッとやっているだけで特に変わったことはやっていない
ramark
の画像 URL 置き換え処理は ↓ のような感じで処理している
export const replaceImagePath = ({ fullPath }: { fullPath: string }) => {
return function transformer(tree: any) {
const visit = (node: any) => {
if (node.type === 'image') {
const imageNumber = node.alt
const result = fullPath.match(/posts\/(\d+)\/(\d+)\/(.+)\/index.md/)
if (!result) return
const [_, year, month, slug] = result
const newUrl = `/post-images/${year}/${month}/${slug}/${imageNumber}.png`
node.url = newUrl
}
if (node.children) {
node.children.forEach(visit)
}
}
visit(tree)
}
}
そしてこのプラグインを、 remark
の処理のチェーンに繋げる
const result = await remark()
.use(remarkGfm)
.use(remarkHtml)
.use(replaceImagePath, { fullPath })
.process(articleBody)
remark
の node
からファイル名とかパスとか取れるかと思ったんだけど、何の情報も取れなかったのでオプションで Markdown のフルパスを渡している
ということで無事に面倒な手作業なく画像を表示できるようになった
ちなみに remark
のプラグインの書き方いまいちわからなかったので、同じように URL をいい感じにしてくれるremark-img-linksのソースコードを見て真似してみた
(これ使えそうだったんだけど、微妙に僕の記事のデータと相性が悪くてダメだった)
URL がリンクになっていない
[URL](URL)
みたいに書かないで、URL をダイレクトに書いている箇所は軒並みただの文字列になってしまっていた
ちょっとググったところ、GitHub の README みたいにいい感じに変換してくれる remark-gfm
というライブラリがあることを知ったので入れてみたところ、無事に単なる URL の記述でリンクになってくれた
https://github.com/remarkjs/remark-gfm
これまた自分で変換するのはあまりにだるいし絶対にやりたくなかったので助かった
それぞれの記事で title タグをいい感じに設定する
全てのページで Mission-Street.
のままだったので直した
generateMetadata
関数を用意して export してやれば OK
https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function
Firebase でホスティングしたら、記事の詳細ページでリロードすると 404 になる
僕のブログの URL は https://hakozaru.com/posts/comeback
みたいになっていて末尾に/はつかない
ブログを移植するにあたって、当然だけど URL は変えたくなかったので、Next.js の設定は特に何も変えずに開発していた
(trailingSlash
を書かなければ、末尾に/は入らず、以前と同じパスで閲覧できる)
しかし、いざ実際に動かしてみようと思って Firebase にデプロイしてみたところ、
index ページは大丈夫だけど、記事の詳細ページ(/posts/hoge)は、最初遷移したときは大丈夫だけど、その後リロードすると 404 になってしまった
言葉で説明するとややこしいので関係をまとめてみた
trailingSlash: true
を入れなかった場合- ビルドすると
/out/posts/:slug.html
が出力される - URL は末尾に/が 入らない
- ビルドすると
trailingSlash: true
を入れた場合- ビルドすると
/out/posts/:slug/index.html
が出力される - URL は末尾に/が 入る
- ビルドすると
なので、URL の希望としては trailingSlash
を入れないのが正解なんだけど、それだと Firebase にデプロイした時に動かない
(そもそも表示して欲しいファイルが /out/posts/:slug
にないので表示できない)
困ったのでどうにかできないか色々ググってみたところ、Firebase の設定にも trailingSlash
の設定があることが判明したので、firebase.json
に設定したところ URL 末尾/なし + リロードしても 404 にならない
を実現できた
{
"hosting": {
"public": "out",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
+ "trailingSlash": false
}
}
Firebase の rewrite の設定で頑張ることもできそうだけど、あまりややこしい設定を増やしたくないので、いい感じにまとまったと思っている
タイトルのフォントが読み込めたり読み込めなかったりする
このサイトのタイトル部分のフォントは Google Fonts のMontserratというのを @import
して使っていたんだけど
なんか最初アクセスするとフォントちゃんと適用されるのに、リロードすると解除されたりして謎の挙動をしていた
しかも全部の画面で発生するのではなく、記事の詳細では全く問題なかったりして、何もワカラナイ状態だった
なので色々対処方法をググっていると、なんと Next.js は GoogleFonts をセルフホスティングして Next 側で最適化しているということが判明した
(完全に見逃していたんだけど、レイアウトファイルで import { Inter } from 'next/font/google'
みたいなのがデフォルトで書かれていた)
ということで以下のように書いて解決した
import { Montserrat } from 'next/font/google'
const montserrat = Montserrat({
subsets: ['latin'],
weight: '900',
display: 'swap',
})
//...
<div className={montserrat.className}>hoge</div>
//...
表示は安定するし、外部への通信は減るし、最高すぎる
シンタックスハイライトがなくなった
まぁコード自体は問題なく見れるしええやろってことで一旦スルーすることにした
remark
向けのライブラリはあるので欲しくなったら入れる