Next.js色々備忘録
デプロイ時のエラーメモ
Vercelへのデプロイエラー
- エラーページを作らなきゃいけないらしい
- 前にNewt&Netrifyの時はこんなの出なかったし、このエラーページは作ってなかった
- https://zenn.dev/links/articles/aa9340d34aea76
- https://github.com/vercel/next.js/issues/23128
Vercel画像最適化上限オーバーエラー
vercelはnext/image
で読み込んだ画像を最適化してくれるけど、無料プランは月1,000が上限
それを超えると警告され、さらに無視するとアプリが停止される
AWS Amplifyへのデプロイエラー
Amplifyは静的HTMLをホスティングしてる?から、buildだけでなくexportも必要っぽい?
- buildは本番モードで出力
- exportは静的HTMLで出力
大文字小文字によるエラー
- ファイル名が
hoge.jsx
- importを
import Hoge from 'Hoge'; // hoge.jsxなのに先頭大文字になってる
みたいにすると、ローカルだと問題無い(大文字小文字が区別されない)が、
デプロイ先によっては大文字小文字が区別されてしまい、module not found
になってしまう。
ローカルでファイルを編集してもブラウザに反映されない時は、大文字小文字がミスってる可能性が高い。
ローカルでも大文字小文字を厳密に区別されるようにするにはプラグインを入れる。
// 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
ローディング
リンククリックしてから再描画までの間、何もレスポンスが無い状態が数秒発生する(コンポーネントの量によっては一瞬で画面遷移することもある)。
それだとユーザビリティ的に良くないからローディングなどを表示させたい。
どうやって実装する?
- 全ての<Link>にイベント仕込む?流石にめんどい。他にいい方法ありそう
- 良さそうなのあった
- https://github.com/apal21/nextjs-progressbar
- 使ってみたらめっちゃ楽に導入できた。readmeそのまま。
- router.events使えばローディング全ページに一括で適用できるっぽい?_appで定義する?
- router.eventsには読み込み開始と終了時のイベントが用意されてる?から、それでローディングの表示/非表示ができるっぽい
- https://zenn.dev/neko/articles/a008430784759d
こんな感じでappでrouter.eventsを使えばLoadingコンポーネントをページ読み込み時に表示させられる
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;
gsap
途中から横スクロールするやつの参考記事
- 横スクロールさせる繰り返し要素を画像のみにした場合、
reset.css
などのimg { width: 100% }
が適用されていると横スクロールが効かないから要注意- 横幅を明示的に指定する必要あり
条件分岐
htmlタグをpropsで出し分ける
export default function PageHeader({ titleHtmlTag = 'h1' }) {
const Title = titleHtmlTag; // propsを直接タグに使うとうまくいかないから一度変数に入れる
return (
<>
<Title>タイトル</Title>
</>
);
}
細かい知識
日付フォーマット
date-fnsを使うと楽
import { format } from 'date-fns';
const formattedDate = format(new Date(apiResponse.date), 'yyyy/MM/dd');
解析タグ
GA4やタグマネは、タグを置くだけじゃだめ。ページ遷移のPVが計測されない。
ページ遷移時に解析タグは変わらないから再レンダリングされない?=発火しないからPVが計測されない。みたいなイメージだと思う。
なので、_app.jsxでrouter.eventsが終わったらイベントを飛ばしてあげる
GA4の参考記事
似たようなことをタグマネでもやってみたが、こっちはうまくいかなかった。
GA4でも最初はうまくいかなくて、完全0から作ったdev環境用のGA4プロパティならうまくいったけど、本番の昔のGAから移行?みたいな感じで作ったGA4プロパティだとうまくいかなくて、結局本番用のも0から新しくGA4プロパティを作ったらうまくいった。
もしかしたら完全0から作ったGA4なら、タグマネもうまくいったのかな?と思った。
プレビュー環境
ヘッドレスCMSにNewtを採用して、プレビュー環境を作ろうとした
ローカルは問題なかったが、デプロイしても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,
};
}