【Next.js】public内のファイルがリンク切れになる

# 結論

いい設定方法が見つからなかったため、結局ページのファイルに直接埋め込むことにしました。

まずはサブディレクトリの設定をします。
`next.config.js`を以下のようにしてください。

```next.config.js
const isProd = process.env.NODE_ENV === 'production';
const basePath = isProd ? 'ここにパスを入れる' : ''; // 例: /sub-directory

/** @type {import('next').NextConfig} */
const nextConfig = {
    publicRuntimeConfig: {
        basePath, // basePathを他のファイルから読み込めるようにする
    }
}

module.exports = nextConfig
```

このコードでは、

1. `isProd`変数に本番環境かどうかが格納される
1. `basePath`変数にサブディレクトリのパスが格納される
1. `publicRuntimeConfig`によって`basePath`がコンポーネント側から参照できるようになる

次に、public内にあるファイルを読み込みたいコード(ここでは`page.tsx`)の最初に、
以下の行を記述します。
```page.tsx
// なんかいろいろなimport文

// 画像ファイルのパス変換用
import getConfig from "next/config";
const { basePath } = getConfig().publicRuntimeConfig;
```

これを書くことによって、`basePath`変数に`next.config.js`で設定したサブディレクトリの文字列が格納されます。
<sub>といっても`next.config.js`に書いた`basePath`変数の値がそのまま入っているだけですが...</sub>

実際にpublic内のファイルを読み込みたい時は、以下のようにして使います。
ここでは　`public/vercel.svg`という画像ファイルを、`Image`コンポーネント(Next.jsに標準で入ってる`img`みたいなやつ)で表示させています。

```page.tsx
// さっき書いたやつとかそれ以外のimportとか
// あとは処理もあるかもしれない

export default function Home() {
  return (
    <main>
      // 略
      <Image
        src= {`${basePath}/vercel.svg`} // basePath変数を埋め込む
        alt="Vercel Logo"
        className={styles.vercelLogo}
        width={100}
        height={24}
        priority
      />
      // 略
    </main>
  )
}
```

ここでは`Image`要素の`src`属性で`basePath`変数を使っています。
`basePath`はただの文字列なので、このようにテンプレートリテラルで埋め込むことができます。

:::note alert
この方法はとても単純なため、手を加えるのは簡単です。
その代わり例外処理やフォーマットの修正は一切やっていません。
なので実際に使うならその辺りの処理を入れたほうがいいと思います。
:::

### 概要: 何がしたかったか

- とりあえず`create next-app`で出てくるページを表示したかった
- サイトを公開する場所を、**開発環境ではルート**に、**本番環境ではサブディレクトリ**にしたかった
- GitHub Pagesで公開したかった

というわけで、<sub>Dockerを使って開発環境を整え、`yarn create next-app`でNext.jsプロジェクトを準備し、`yarn dev`でページが正常に表示されていることを確認し、</sub>GitHub Pagesで公開しました。
デプロイが成功して喜んだのも束の間、公開されたページを見たら、中央にあるはずの **NEXT.svgという画像が表示されていません**でした。

なんで？と思って`src`属性を見たら、**なんと`/next.svg`と書かれている**ではありませんか！！(泣)
実際に画像があるのは`/nextjs-page/next.svg`なのに...
もちろん右上の方にある`vercel.svg`も表示されていませんでした。

画像の公開先は変えられません(たぶん)。
なので変えるなら画像の`src`属性のほうです。
画像は`nextjs-page`というサブディレクトリにあるので、**パスの最初に`/nextjs-page`を追加**すればうまく表示されるようになります。

ということで上の方法の登場です。

1. `next.config.js`で**サブディレクトリの設定をする**
1. 画像(などの`/public`内にあるファイル)を読み込みたいコンポーネントがあるファイルの上の方に、**2行ほどコードを追記する**
1. テンプレートリテラル(``)を使って**パスを修正**

色々考えた結果こうなりました。
~~本当にどうすればよかったのか...~~


### 環境

* MacOS Sonoma 14.1 (Apple M1)
* dockerを使用(`node:18.17.1`)
* yarn 1.22.19
* Next.js 14.0.4
* TypeScript 5.3.3
* React 18.2.0


# ことの経緯

もともと、以下の記事を参考にしてNext.jsのページを公開しようとしていました。

https://qiita.com/unreadabread/items/920420c24fc49cb3b392

この記事はタイトルの通り、Next.jsで作ったサイトをGitHub Pagesで公開するというものです。

> 序文より引用：
> 
> この記事は
> 1. create-next-app で Next.js アプリを作成
> 1. GitHub にプッシュ
> 1. GitHub Actions でビルド＆ GitHub Pages にデプロイ
>
> までの工程をバカみたいに丁寧に書き起こしたものです。

という感じでやっていました。


## リポジトリ名は変更

上の記事には以下のように書いてあります。
> **`Repository name` は必ず`ユーザー名.github.io`にして、
> `Public` を選択してください。**
>
> `Repository name` を任意の名前にすると `next.config.js` の `basePath` などを変更する必要が出てきます。(要検索)

とのことなのですが、残念ながら私は諸事情により`ユーザー名.github.io`という名前のリポジトリが作れませんでした。
そのため、リポジトリ名は`nextjs-page`でいくことにしました。

:::note alert
察しの良い方は気づいていると思いますが、画層がうまく表示されなかったのはこれが原因です。
:::


## ファイル構成

こんな感じになりました。

```sh
nextjs-page
├── .github
│   └── workflows
│       └── nextjs.yml
├── .next
├── README.md
├── compose.yaml
├── dockerfile
├── .gitignore
├── .eslintrc.json
├── next-env.d.ts
├── next.config.js
├── package.json
├── public # 画像などのファイルはここ
│   ├── next.svg
│   └── vercel.svg
├── src
│   └── app # ページのファイルはここ
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       ├── page.module.css
│       └── page.tsx
├── tsconfig.json
└── yarn.lock
```

`create-next-app`を叩くと出てくるファイルそのままです。
GitHub Actionsを使うためのファイルがありますが、それ以外に変更点はないと思います。

## いろいろ調べる

画像が表示されてない！！なんで！？
と思い、いろいろ調べました。

原因は前述の通り`src`属性のパスが間違っていることなのですが、じゃあどうやって解決したら良いのかがなかなか出てきませんでした。

まず目にしたのが、`basePath`と`assetPrefix`でした。


### basePathとassetPrefix

この二つは何かというと、`next.config.js`に書く設定です。

```next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export', // これはGitHub Pagesで公開するために必要
    basePath: 'ここに設定を書く',
    assetPrefix: 'ここに設定を書く',
}

module.exports = nextConfig
```

- `basePath`：すべてのベースとなるパス。
- 設定したパスにページがデプロイされるようになる。
ここに設定するのは`/`から始まる文字列。`/nextjs-page`など。
詳しくは[こちら](https://nextjs.org/docs/app/api-reference/next-config-js/basePath)。
- `assetPrefix`：アセットファイル(CSSなど)のパス。
`/public`にある画像ファイルなどのパスは影響を受けない。
ここに設定するのはURLか`/`から始まる文字列。
詳しくは[こちら](https://nextjs.org/docs/app/api-reference/next-config-js/assetPrefix)。

この二つをいじればうまく画像が表示されるようになるのではないかと思って、いろいろ試そうと思いました。
ですが、よく考えたらこのバグはローカル環境では発生していません。
なのでいろいろ試す前にローカル環境でバグを再現します。

:::note warn
最終的な解決法方にこの二つが登場していないところから察せるかもしれませんが、この二つをいじっても解決しませんでした。
ですが一応、その実験の過程は書いておこうと思います。
:::


### ローカルでバグを再現する

まず、このバグはページの公開先が`/nextjs-page`になっていることによって起こっています。
Next.jsのページはデフォルトだと`/`にデプロイされるので、これを設定で変えないといけません。
そこで、`basePath`を使います。

上に書いた`basePath`という設定ですが、これを変えるとパスのベースが変わります。
例えば`basePath: '/nextjs-page'`という行を`next.config.js`に追記すると、
デフォルトで用意されているページが`localhost:3000`ではなく`localhost:3000/nextjs-page`に表示されるようになります。

なので`next.config.js`を編集します。

```diff_javascript:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export', // GitHub Pagesで公開するために必要
+   basePath: '/nextjs-page'
}

module.exports = nextConfig
```

この行を追記するだけであら不思議、画像がリンク切れで表示されなくなり、コンソールには404エラーが出力されるようになります。
これでバグの再現ができました。


### いろいろ試してみる

というわけで、`basePath`と`assetPrefix`それぞれで設定をしたときとしていないときの違いを見てみました。

下はその結果をまとめた表です。

| 組み合わせ | ページの場所 | 読み込んでいるアセット | 読み込んでいる画像 | 実際のアセット | 実際の画像 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| どっちも指定 | /nextjs-page | /nextjs-page | / | /nextjs-page | /nextjs-page |
| basePathのみ | /nextjs-page | /nextjs-page | / | /nextjs-page | /nextjs-page |
| assetPrefixのみ | / | /nextjs-page | / | / | / |
| 未指定 | / | / | / | / |/ |

* 「指定」と書いてあるところで指定しているパスは`/nextjs-page`です。
* 「読み込んでいるアセット」とは、実際に表示されるHTMLの該当するタグの、
パスが書かれている属性の値です。
* 「読み込んでいる画像」も上と同じです。
* [自分のissue](https://github.com/nanasi-1/nextjs-page/issues/1)からのコピペです。

上の表から読み取れることは、こんな感じでしょうか。

* 「読み込んでいる画像」はどちらの影響も受けない
* 「読み込んでいるアセット」はどちらの影響も受ける
* `basePath`を変えるとアセットや`/public`内のファイルの公開先が変わる

どうやら悲しいことに、Next.jsでは　`/public`内にあるファイルのパスの自動修正等は行われないみたいです。


## 自力で実装

さて、良い感じの方法はなさそうだ。
じゃあどうするか？自力でそれっぽい実装をするしかない。
ということでやっていきます。

とりあえず`basePath`あたりの情報を漁っていた時に見つけた以下の記事を参考にしました。
`basePath`と`assetPrefix`あたりの設定も書いてあったので助かりました。

https://qiita.com/hiropy0123/items/02ab91f69dbfa4e2797f

そして登場するのが`publicRuntimeConfig`です。


### publicRuntimeConfig

これは何かというと、私もよくわかってないのですが、ここに書いた値はコンポーネント側から参照できるみたいです。

例えばこんな感じにします。

```next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
    publicRuntimeConfig: {
        example: 'さんぷるでーた',
    }
}

module.exports = nextConfig
```

上のようにすると、コンポーネント側からこの設定が見れます。

```page.tsx
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig(); // serverRuntimeConfigもあった

console.log(publicRuntimeConfig.example); // さんぷるでーた
```

詳しくは以下のページをご覧ください。

https://nextjs-ja-translation-docs.vercel.app/docs/api-reference/next.config.js/runtime-configuration

:::note warn
上のページを見てみたら、こんなことが書いてありました。
> 一般的には、[環境変数](https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/next.config.js/environment-variables) を使用して設定を行うのがいいでしょう。なぜなら、ランタイム設定はレンダリング / 初期化時の余計な処理につながり、また[Automatic Static Optimization](https://nextjs-ja-translation-docs.vercel.app/docs/advanced-features/automatic-static-optimization)と互換性がないからです。

どうやら環境変数の方がいいみたいです。
環境変数については全然分からないので解説できないです。すみません...
環境変数を使う場合は自力で調べてください。
:::


### 実際のページで使う

:::note info
といってもここで出来上がったのが冒頭に書いたソースコードです。
なのであんまり書くことないです。
:::

では、この`publicRuntimeConfig`を使ってサブディレクトリをコンポーネント側で取得してみます。

まずは`next.config.js`にサブディレクトリの情報を書きます。

```diff_javascript:next.config.js
+ const basePath = '/nextjs-page'; // 変数にした

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
    basePath, // きっと元からあった。これがどことのdiffなのかとか気にしない。
    publicRuntimeConfig: {
-       example: 'さんぷるでーた',
+       basePath,
    }
}

module.exports = nextConfig
```

そしたらコンポーネント側で使います。
上の方に2行コードを追記したら準備完了です。
~~<sub>グローバル変数？しらんがな</sub>~~

```diff_typescript:/src/app/page.tsx
import Image from 'next/image'
import styles from './page.module.css'

+ import getConfig from "next/config";
+ const { basePath } = getConfig().publicRuntimeConfig;

export default function Home() {
  return (
    <main>
      // 略
      <Image
-       src= "/vercel.svg"
+       src= {`${basePath}/vercel.svg`} // basePath変数を埋め込む
        alt="Vercel Logo"
        className={styles.vercelLogo}
        width={100}
        height={24}
        priority
      />
      // 略
    </main>
  )
}
```

詳しくは[冒頭のコード解説](#結論)を見てください。
上のようにすると、無事にリンク切れがなくなります。やったね。


### ローカルではルートにデプロイする

今のままだとバグの再現のためにローカル環境でもルートにサイトがデプロイされてしまうので、これを元に戻したいと思います。

これをするためには、ローカル(開発)環境のときと本番環境の時で処理を変えなければいけません。
何をもってして判断するのか？というと、`NODE_ENV`という環境変数の値です。

```next.config.js
// NODE_ENV環境変数がproduction(=本番環境)かどうか
const isProd = process.env.NODE_ENV === 'production';

// 本番環境ならサブディレクトリを'/nextjs-page'に、
// そうじゃないなら''にする
const basePath = isProd ? '/nextjs-page' : '';
```

こんな感じにすることで、`NODE_ENV`の値によってサブディレクトリを変えることができます。

最終的に出来上がるのはこんな感じです。(冒頭のコードと同じとも言う)
```diff_javascript:next.config.js
const isProd = process.env.NODE_ENV === 'production';
const basePath = isProd ? '/nextjs-page' : '';

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
-   basePath,
    publicRuntimeConfig: {
        basePath, // basePathを他のファイルから読み込めるようにする
    }
}

module.exports = nextConfig
```

本番環境であるGitHub Pagesでは、`basePath`という設定が空でも`/nextjs-page`(リポジトリ名)にデプロイされるため、`basePath`という設定の行は消しています。
<sub>変数名も`basePath`なのややこしいって？何も考えてなかったんだよ！！(後悔)</sub>

これで、ローカルでは`/`にデプロイされるようになりました。
また、`page.tsx`に変更を加えたことにより、画像のリンク切れも直りました。


# 参考

https://qiita.com/hiropy0123/items/02ab91f69dbfa4e2797f

https://qiita.com/unreadabread/items/920420c24fc49cb3b392