5
0

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

Posted at

結論

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

まずはサブディレクトリの設定をします。
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変数に本番環境かどうかが格納される
  2. basePath変数にサブディレクトリのパスが格納される
  3. publicRuntimeConfigによってbasePathがコンポーネント側から参照できるようになる

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

page.tsx
// なんかいろいろなimport文

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

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

実際に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はただの文字列なので、このようにテンプレートリテラルで埋め込むことができます。

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

概要: 何がしたかったか

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

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

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

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

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

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

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

環境

  • 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のページを公開しようとしていました。

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

序文より引用:

この記事は

  1. create-next-app で Next.js アプリを作成
  2. GitHub にプッシュ
  3. GitHub Actions でビルド& GitHub Pages にデプロイ

までの工程をバカみたいに丁寧に書き起こしたものです。

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

リポジトリ名は変更

上の記事には以下のように書いてあります。

Repository name は必ずユーザー名.github.ioにして、
Public を選択してください。

Repository name を任意の名前にすると next.config.jsbasePath などを変更する必要が出てきます。(要検索)

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

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

ファイル構成

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

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属性のパスが間違っていることなのですが、じゃあどうやって解決したら良いのかがなかなか出てきませんでした。

まず目にしたのが、basePathassetPrefixでした。

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など。
    詳しくはこちら
  • assetPrefix:アセットファイル(CSSなど)のパス。
    /publicにある画像ファイルなどのパスは影響を受けない。
    ここに設定するのはURLか/から始まる文字列。
    詳しくはこちら

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

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

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

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

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

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

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

module.exports = nextConfig

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

いろいろ試してみる

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

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

組み合わせ ページの場所 読み込んでいるアセット 読み込んでいる画像 実際のアセット 実際の画像
どっちも指定 /nextjs-page /nextjs-page / /nextjs-page /nextjs-page
basePathのみ /nextjs-page /nextjs-page / /nextjs-page /nextjs-page
assetPrefixのみ / /nextjs-page / / /
未指定 / / / / /
  • 「指定」と書いてあるところで指定しているパスは/nextjs-pageです。
  • 「読み込んでいるアセット」とは、実際に表示されるHTMLの該当するタグの、
    パスが書かれている属性の値です。
  • 「読み込んでいる画像」も上と同じです。
  • 自分のissueからのコピペです。

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

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

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

自力で実装

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

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

そして登場するのが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); // さんぷるでーた

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

上のページを見てみたら、こんなことが書いてありました。

一般的には、環境変数 を使用して設定を行うのがいいでしょう。なぜなら、ランタイム設定はレンダリング / 初期化時の余計な処理につながり、またAutomatic Static Optimizationと互換性がないからです。

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

実際のページで使う

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

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

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

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

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

module.exports = nextConfig

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

/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の値によってサブディレクトリを変えることができます。

最終的に出来上がるのはこんな感じです。(冒頭のコードと同じとも言う)

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という設定の行は消しています。
変数名もbasePathなのややこしいって?何も考えてなかったんだよ!!(後悔)

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

参考

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0