📝

Next 14で静的ビルド(next export)した時にハマったこと備忘録

2024/07/16に公開

概要

久々にnextでの開発を仕様として静的サイト作成時に詰まったので解決した記録を残す

next.configはこれ

/** @type {import('next').NextConfig} */

const nextConfig = {
    distDir: 'build',
    output: 'export',
    sassOptions: {
    },
    pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
    compiler: {
        styledComponents: true,
    },
};

export default nextConfig;

問題1: Page "xxxx" is missing "generateStaticParams()"

Next14で静的ビルドする時、動的なページパス(page/[id])とかは明示的にどんなパスが生成されるか、generateStaticParams という関数をページのファイルにこちらで定義しないといけないらしい
公式doc https://nextjs.org/docs/app/api-reference/functions/generate-static-params

詳しい例

例えば記事のIDごとに articles/[id] というURLで動的なページを作りたいとき

ファイル内にパラメータとして返す値を定義した配列を返す関数を generateStaticParams という名前で定義しないといけない

import STArticle from '@/cocmponents/templates/STArticle/STArticle'
import { ARTICLE_LIST } from '@/const/articles/article_list'

// 記事にアクセスする際のパラメータとして使用される値を配列として返す関数をgenerateStaticParamsという命名で定義する必要がある
export const generateStaticParams = () => {
  return ARTICLE_LIST.map((article) => ({
    id: article.id,
  }));
/**
 ↑は記事の配列から返してるけど下記の配列が返ってくればいい
 [{id: 'hoge'}, {id: 'fuga'}]
 id の部分はディレクトリ名として定義した命名([id])と一致させる
*/
};

const ArticlePage = ({params}) => {
  const articleId = params.id
  const ARTICLE = ARTICLE_LIST.find((article) => article.id === articleId)
  return <>
    <STArticle article={ARTICLE}/>
  </>
}
 
export default ArticlePage

問題2: Error: Could not find a production build in the 'build' directory. Try building your app with 'next build' before starting the production server.

概要

next build というコマンドでbuildというディレクトリに静的ファイルが生成されても next start 時に 「buildファイルが見つからない」という意味のエラーになる

結論

静的サイト生成時(nuext.configでmode: exportにしているとき)は nuxt start を使わない
ローカルでサーバを実行して生成したディレクトリを配信すればいい
npx serve@latest 生成したディレクトリ

解決ログ

エラーが起こっているライブラリ内のコードでは BUILD_IDというファイルを読みに行ってエラーになっている

\Users\xxx\node_modules\next\dist\server\lib\router-utils\filesystem.js
        try {
            // ここでエラーが起こってる
            buildId = await _promises.default.readFile(buildIdPath, "utf8");
        } catch (err) {
            if (err.code !== "ENOENT") throw err;
            throw new Error(`Could not find a production build in the '${opts.config.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id`);
        }

↓エラーの内容をconsole.logしてみた

[Error: ENOENT: no such file or directory, open 'C:\Users\xxx\build\BUILD_ID'] {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: 'C:\\Users\\xxx\\build\\BUILD_ID'
}

C:\Users\xxx\build\BUILD_ID というファイルががいないと言われてるけど
実際にnext buildで生成されたファイルにも build/BUID_IDはいない

試しに build/BUULD_ID ファイルを作ってみるとError: ENOENT: no such file or directory, scandir 'C:\xxx\build\static' エラーが出る
staticを _nextから移すと今度はError: ENOENT: no such file or directory, open 'C:\xxx\build\routes-manifest.json'というエラーが出る

ここからそもそもbuild時に生成される生成物が静的サイト用の物ではない可能性がわかる
next.configdistDir: 'build'が悪いのかと思い消して、next build > next start してみる

/** @type {import('next').NextConfig} */

const nextConfig = {
    output: 'export',
    sassOptions: {
    },
    pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
    compiler: {
        styledComponents: true,
    },
};

export default nextConfig;

下記エラーが出る

Error: "next start" does not work with "output: export" configuration. Use "npx serve@latest out" instead.

npx serve@latest out を実行してみる

serveできた

distDir: 'build' を再度next.configに定義して試してみる > 配信できる

↓このメッセージは生成ディレクトリがデフォルトの out 以外の時は出てくれないんかい…
Error: "next start" does not work with "output: export" configuration. Use "npx serve@latest out" instead.

問題3: 生成された静的ディレクトリをローカルサーバで配信しても画像が表示されない

結論

静的画像では import Image from 'next/image' ではなく img タグを使う

    {/* eslint-disable @next/next/no-img-element*/}
    <img
      alt={props.alt}
      src={imgData}
      width="100"
      height="100"
      sizes="100vw"
      loading="lazy"
      style={{
        width: '100%',
        height: 'auto',
      }}/>
      {/* eslint-enable @next/next/no-img-element*/}

ちなみにimgタグでonLoadが効かなかったので仕方なくnew Imageでの画像読み込み完了を判定しました↓

  const [isLoadingEnd, setIsLoadingEnd] = useState(false)
  const [isLoadingStart, setIsLoadingStart] = useState(false)

  const loadImage = useCallback(async () => {
    try {
      setIsLoadingStart(true)
      const RenderImage = new Image()
      RenderImage.src = props.src
      await RenderImage.decode()
    } catch (e) {
      console.log(e)
    }
    setIsLoadingEnd(true)
  }, [props.src])

  useEffect(() => {
    if(!isLoadingStart) {
      loadImage()
    }
  }, [isLoadingStart, loadImage])

解決ログ

画像自体は存在していている

ブラウザでは_nuxt/imageのパスを見に行ってしまっていてエラーになっている?

他記事では Image ではなく素のimgタグを使用することで解決していた
https://zenn.dev/kou7273/articles/202304210108

Discussion