Open7

Next.js色々備忘録

dadadadadada

デプロイ時のエラーメモ

Vercelへのデプロイエラー

Vercel画像最適化上限オーバーエラー

vercelはnext/imageで読み込んだ画像を最適化してくれるけど、無料プランは月1,000が上限

それを超えると警告され、さらに無視するとアプリが停止される

https://qiita.com/0ba/items/46709d24bc81efe1cb6c

AWS Amplifyへのデプロイエラー

Amplifyは静的HTMLをホスティングしてる?から、buildだけでなくexportも必要っぽい?

  • buildは本番モードで出力
  • exportは静的HTMLで出力

https://www.codegrid.net/articles/2021-nextjs-1/

大文字小文字によるエラー

  • ファイル名がhoge.jsx
  • importをimport Hoge from 'Hoge'; // hoge.jsxなのに先頭大文字になってる

みたいにすると、ローカルだと問題無い(大文字小文字が区別されない)が、
デプロイ先によっては大文字小文字が区別されてしまい、module not foundになってしまう。

ローカルでファイルを編集してもブラウザに反映されない時は、大文字小文字がミスってる可能性が高い。

ローカルでも大文字小文字を厳密に区別されるようにするにはプラグインを入れる。
https://www.npmjs.com/package/case-sensitive-paths-webpack-plugin

// next.config.js
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const nextConfig = {
  reactStrictMode: true,
  webpack(config, options) {
    config.plugins.push(new CaseSensitivePathsPlugin());
    return config;
  },
}
module.exports = nextConfig
dadadadadada

ローディング

リンククリックしてから再描画までの間、何もレスポンスが無い状態が数秒発生する(コンポーネントの量によっては一瞬で画面遷移することもある)。

それだとユーザビリティ的に良くないからローディングなどを表示させたい。

どうやって実装する?

  • 全ての<Link>にイベント仕込む?流石にめんどい。他にいい方法ありそう
  • 良さそうなのあった
  • router.events使えばローディング全ページに一括で適用できるっぽい?_appで定義する?

こんな感じでappでrouter.eventsを使えばLoadingコンポーネントをページ読み込み時に表示させられる

_app.jsx
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import Loading from '@/components/ui/Loading/Loading';

function MyApp({ Component, pageProps }) {
  const router = useRouter();
  const [nowLoading, setLoading] = useState(false);
  useEffect(() => {
    const handleStart = () => setLoading(true);
    const handleComplete = () => setLoading(false);
    router.events.on('routeChangeStart', handleStart);
    router.events.on('routeChangeComplete', handleComplete);
    router.events.on('routeChangeError', handleComplete);
    return () => {
      router.events.off('routeChangeStart', handleStart);
      router.events.off('routeChangeComplete', handleComplete);
      router.events.off('routeChangeError', handleComplete);
    };
  }, [router]);

  return (
    <>
      {nowLoading && <Loading />}
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

dadadadadada

gsap

途中から横スクロールするやつの参考記事
https://zenn.dev/thetalemon/articles/f759b1acd0053a

  • 横スクロールさせる繰り返し要素を画像のみにした場合、reset.cssなどのimg { width: 100% }が適用されていると横スクロールが効かないから要注意
    • 横幅を明示的に指定する必要あり
dadadadadada

条件分岐

htmlタグをpropsで出し分ける

export default function PageHeader({ titleHtmlTag = 'h1' }) {
  const Title = titleHtmlTag; // propsを直接タグに使うとうまくいかないから一度変数に入れる
  return (
    <>
        <Title>タイトル</Title>
    </>
  );
}
dadadadadada

細かい知識

日付フォーマット

date-fnsを使うと楽

import { format } from 'date-fns';
const formattedDate = format(new Date(apiResponse.date), 'yyyy/MM/dd');
dadadadadada

解析タグ

GA4やタグマネは、タグを置くだけじゃだめ。ページ遷移のPVが計測されない。
ページ遷移時に解析タグは変わらないから再レンダリングされない?=発火しないからPVが計測されない。みたいなイメージだと思う。

なので、_app.jsxでrouter.eventsが終わったらイベントを飛ばしてあげる

GA4の参考記事
https://zenn.dev/rh820/articles/8af90011c573fe


似たようなことをタグマネでもやってみたが、こっちはうまくいかなかった。

GA4でも最初はうまくいかなくて、完全0から作ったdev環境用のGA4プロパティならうまくいったけど、本番の昔のGAから移行?みたいな感じで作ったGA4プロパティだとうまくいかなくて、結局本番用のも0から新しくGA4プロパティを作ったらうまくいった。

もしかしたら完全0から作ったGA4なら、タグマネもうまくいったのかな?と思った。

dadadadadada

プレビュー環境

ヘッドレスCMSにNewtを採用して、プレビュー環境を作ろうとした
https://www.newt.so/docs/tutorials/nextjs-preview-mode

ローカルは問題なかったが、デプロイしてもAPIのルーティング(/api/preview/...)が404になってしまう。

デプロイ先はAWS Amplify。

Amplifyは静的ファイルのみを扱う環境なので、SSR(GetServerSideProps)が動かないみたい。
色々ぐぐってみて、amplifyの設定ファイルにそれっぽい記述追加してもだめだった。

なので、結局GetServerSidePropsでやるのは諦めて、pages/news/previewのようなルーティングを作って、ページ読み込み時にNewtのAPIを叩いてデータを取得する形式にした。

呼び出し先
import { getNewsPost } from '@/features/news/api/getNewsPost'; // 投稿を取得するメソッド
import useCheckDraftPost from '@/hooks/useCheckDraftPost';

export default function NewsSingle({}) {
  const { postDetail, loading } = useCheckDraftPost(getNewsPost);
  if (loading) {
    return <p>Loading...</p>;
  }
  return (
    <NewsDetail data={postDetail} />
  )
}
フック
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

/**
 * 下書き用の投稿を表示するフック
 * パラメータのsecretがenvで定義した値と一致しない場合、トップページへリダイレクトさせる(不正アクセス防止)
 * 以下のようにアクセスするイメージ
 *   /path/to/?slug=記事スラッグ&secret=envで定義した値
 * @param {Function} getPost 投稿を取得するためのメソッド
 * @returns {Object} postDetail 投稿詳細のデータ
 * @returns {Boolean} loading データ取得中ならTrue
 */
export default function useCheckDraftPost(getPost) {
  const router = useRouter();
  const [postDetail, setPostDetail] = useState(null); // 投稿データ
  const [loading, setLoading] = useState(true); // データの読み込みが完了したらFalseになる
  const redirectIfSecretParamMismatch = (secret, router) => {
    if (secret !== process.env.NEXT_PUBLIC_PREVIEW_SECRET_KEY) {
      router.push('/');
    }
  };

  useEffect(() => {
    const fetchData = async () => {
      // ルーティングが完了し、すべてのクエリパラメータが利用可能になったタイミングで、以下の処理を実行
      if (router.isReady) {
        const { secret, slug } = router.query;
        // envで定義した値と一致しない場合、トップページへリダイレクト
        if (secret !== process.env.NEXT_PUBLIC_PREVIEW_SECRET_KEY) {
          router.push('/');
          return;
        }
        // データ取得
        const post = await getPost(
          {
            slug: slug,
            limit: '1',
          },
          true,
        );
        const postDetail = post.items[0];
        // slugに紐づくデータが無かった場合、もしくはslugパラメータが空の場合、トップページへリダイレクト
        if (!postDetail || !slug) {
          router.push('/');
          return;
        }
        setPostDetail(postDetail);
        setLoading(false);
      }
    };
    fetchData();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [router.isReady]);

  return {
    postDetail: postDetail,
    loading: loading,
  };
}