2023-08-26

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 へ移動など絶対にしたくなかったので、ビルド時に以下のような処理を入れて回避することにした

  1. posts/**/*.png に該当する画像ファイルを /public/post-images/yyyy/mm/:slug/*.png にコピーするようなスクリプトを用意
  2. 1 のスクリプトを npm script に prebuild として登録(こうすると npm run build した時に自動的に処理してくれる)
  3. 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)

remarknode からファイル名とかパスとか取れるかと思ったんだけど、何の情報も取れなかったのでオプションで 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 向けのライブラリはあるので欲しくなったら入れる