どうもこんばんわ。
特に書くことがないのでお正月にWSL 2
で遊んでた時の画像でもおいておきます。いつかWSL 2
の記事も書きたい!
本題
Next.js 15
をやろうとして気付いたらあけおめ。React 19
もRC
が外れたのでいい加減やります。ついでに修正とかもしたいけど今は移行だけします。
見た感じそんな複雑じゃ無さそうなので、git
でいつでも戻せることを確認した上で、codemod
を叩いてみる。
npx @next/codemod@canary upgrade latest
Yes
、y
で。
Need to install the following packages:
@next/codemod@15.1.1-canary.25
Ok to proceed? (y) y
お、Turbopack
が選べる?
でもこのNext.js 製ブログ
はwebpack
に依存してたような、、、
? Enable Turbopack for next dev? » (Y/n)
複数の修正をやってくれるそう。矢印上下キーで移動、スペースでチェックのON/OFF
、A キー
ですべて選択、Enter キー
でcodemod
実行。
React 19
側のcodemod
もやるか?。やるか。
? Would you like to run the React 19 upgrade codemod? » (Y/n)
? Would you like to run the React 19 Types upgrade codemod? » (Y/n)
進めてるとデプロイ先にVercel
を選んでいるか聞かれた。
静的サイトかつAWS
なのでn
で。
? Is your app deployed to Vercel? (Required to apply the selected codemod) » (Y/n)
なんか入れないといけないらしい。いれるか。
y
でエンター。
Need to install the following packages:
types-react-codemod@3.5.2
Ok to proceed? (y)
終わった。
✔ Codemods have been applied successfully.
Please review the local changes and read the Next.js 15 migration guide to complete the migration.
https://nextjs.org/docs/canary/app/building-your-application/upgrading/version-15
差分
page.tsx
とroute.tsx
が
// todo github compare はる
https://github.com/takusan23/ziyuutyou-next/commit/008f234a9ecc215f5d2afc43b469cbac40bc2f04
package.json
codemod
を叩いた際にTurbopack
有効を選んだので、開発時はTurbopack
が有効になるオプションが指定されています。
"scripts" : {
"dev" : "next dev --turbopack" ,
あと一番下になにか追加されてる。
"overrides" : {
"@types/react" : "19.0.2"
}
props が Promise 経由で渡される
ページのURL パス
(動的ルーティングのあれ)とかを関数の引数にProps
としてつけていましたが、このProps
がPromise<Props>
のように、非同期でURL
のパラメータとかが渡ってくるようになりました。
手動で直す場合は、まず各ページの引数の型をPromise<>
でかこって、
async function
でpage.tsx
を書いている場合はawait props
すれば良さそう(codemod
がそうしてた)
https://nextjs.org/docs/app/building-your-application/upgrading/version-15#asynchronous-page
function
でpage.tsx
している場合は、Promise
をReact
から読み取るReact 19
の新機能、use()
のHook
を使えば良いって書いてあります。
https://nextjs.org/docs/app/building-your-application/upgrading/version-15#synchronous-page
以下async function
なpage.tsx
を直した際の差分。
diff --git a/app/posts/[blog]/page.tsx b/app/posts/[blog]/page.tsx
index 228d44e..d6cc1a7 100644
--- a/app/posts/[blog]/page.tsx
+++ b/app/posts/[blog]/page.tsx
@@ -14,11 +14,12 @@ import "../../../styles/css/content.css"
/** 動的ルーティング */
type PageProps = {
- params: { blog: string }
+ params: Promise<{ blog: string }>
}
/** head に値を入れる */
-export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
+export async function generateMetadata(props: PageProps): Promise<Metadata> {
+ const params = await props.params;
const markdownData = await ContentFolderManager.getBlogItem(params.blog)
const ogpTitle = `${markdownData.title} - ${EnvironmentTool.SITE_NAME}`
const ogpUrl = `${EnvironmentTool.BASE_URL}${markdownData.link}`
@@ -41,7 +42,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
* 記事本文。
* 反映されない場合はスーパーリロードしてみてください。
*/
-export default async function BlogDetailPage({ params }: PageProps) {
+export default async function BlogDetailPage(props: PageProps) {
+ const params = await props.params;
// サーバー側でロードする
const markdownData = await ContentFolderManager.getBlogItem(params.blog)
Route Handlers (API Routes)
のroute.ts (.tsx)
に関しても同様にPromise
でProps
を渡してくれる設計だそうです。
このサイトではOGP画像
の生成で使ってて、静的書き出し時にGET
リクエストされOGP画像
も静的書き出しされます。
diff --git a/app/posts/[blog]/opengraph-image.png/route.tsx b/app/posts/[blog]/opengraph-image.png/route.tsx
index af60d1a..488e623 100644
--- a/app/posts/[blog]/opengraph-image.png/route.tsx
+++ b/app/posts/[blog]/opengraph-image.png/route.tsx
@@ -7,7 +7,7 @@ import FileReadTool from "../../../../src/FileReadTool"
/** 動的ルーティング */
type PageProps = {
- params: { blog: string }
+ params: Promise<{ blog: string }>
}
/**
@@ -17,7 +17,8 @@ type PageProps = {
* 使える CSS は以下参照:
* https://github.com/vercel/satori
*/
-export async function GET(_: Request, { params }: PageProps) {
+export async function GET(_: Request, props: PageProps) {
+ const params = await props.params;
// 記事を取得
const markdownData = await ContentFolderManager.getBlogItem(params.blog)
私の環境では以上で、codemod
がすべて直してくれました。
実行できない
webpack
に依存してたような、、、それでTurbopack
にしたから動かないのかも。
React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
⨯ [Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.] {
digest: '356132257'
}
SVGR で webpack を使っていたので修正する
https://nextjs.org/docs/app/api-reference/turbopack
SVGR
はSVG
のファイルをReact
のコンポーネントとして表示できるやつで、
import Icon from 'icon.svg'
みたいにsvg
をimport
するだけで、React
のコンポーネントとして<Icon />
みたいに使えるあれ。便利。
Turbopack
はwebpack
使えないものだと思ってたんですが、Turbopack
でも@svgr/webpack
は動くって言ってる。ん?
でも手元で動いてないんだよな
https://nextjs.org/docs/app/api-reference/config/next-config-js/turbo
どうやらこの通りに書くと動くらしい。やってみる
next.config.js
/** @type {import('next').NextConfig} */
module . exports = {
// この webpack と
webpack ( config ) {
// SVG をコンポーネントにできる
config . module . rules . push ({
test: / \. svg $ / ,
use: [ '@svgr/webpack' ],
})
return config
},
// これを追加
experimental: {
turbo: {
rules: {
'*.svg' : {
loaders: [ '@svgr/webpack' ],
as: '*.js' ,
},
},
}
}
}
開発モードで動いた
Turbopack
、速い。。と思う。私の作りが悪い可能性も十分あるけど。
静的書き出し出来ない
このサイトはNext.js
の output: 'export'
で動いています。
書き出した成果物をS3
にあげCloudFront
でお届けしています。
サイトマップ
npm run build
したら次のエラーが
✓ Linting and checking validity of types
Collecting page data ..Error: export const dynamic = "force-static"/export const revalidate not configured on route "/sitemap.xml" with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export
at <unknown> (C:\Users\takusan23\Desktop\Dev\NextJS\ziyuutyou-next\.next\server\app\sitemap.xml\route.js:1:1127)
sitemap.ts
を静的書き出ししようとしたときのエラー。
export const dynamic
かexport const revalidate
が無いから怒られてる?
export const dynamic = "force-static"
とかは、Next.js
自体をサーバーで動かす場合(Vercel
、レンタルサーバー
、その他)に使うやつで、
あらかじめ生成する静的書き出しモードだと別に指定しなくて良いんじゃないの?って思ってたけど明示的にforce-static
って書かないとだめなのかな。
静的書き出しだとビルドコマンドを叩いた時点ですべてのHTML
が生成されるので、書かずとも全部force-static
になるけど。
一応Issue
見に行ったらあった。以下のようにforce-static
の一文をとりあえず足したら直った。SSG
なのに足さないと行けないのか・・?
https://github.com/vercel/next.js/issues/68667
// 静的書き出しなので指定する必要がないはずだが、Next.js 15 から無いとエラーになってしまう
// https://github.com/vercel/next.js/issues/68667
export const dynamic = "force-static"
/**
* サイトマップを生成する。Next.js 単体で作れるようになった。
* Trailing Slash が有効なので最後にスラッシュ入れました。
*/
export default async function sitemap (): Promise < MetadataRoute . Sitemap > {
// 以下省略...
}
OGP 画像に SVG を描画したらエラー
次はこれ。
Image data URI resolved without size:data:image/svg+xml;,<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"> ... </svg>
Image data URI resolved without size:data:image/svg+xml;,<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"> ... </svg>
Error occurred prerendering page "/posts/amairo_kotlin_coroutines_flow/opengraph-image.png". Read more: https://nextjs.org/docs/messages/prerender-error
Error: SVG data parsing failed cause invalid attribute at 1:44192 cause expected '"' not '<' at 1:44219
at imports.wbg.__wbg_new_15d3966e9981a196 (file:///C:/Users/takusan23/Desktop/Dev/NextJS/ziyuutyou-next/node_modules/next/dist/compiled/@vercel/og/index.node.js:18406:17)
ちなみにこれは開発時でも同じエラーがでました。開発時よりも↑の本番のほうがまだわかりやすいエラーなの、開発時はTurbopack
とかが影響してんのかな。
まあ今まで動いてたので分かんないんだけど。
⨯ [TypeError: The "payload" argument must be of type object. Received null] {
code: 'ERR_INVALID_ARG_TYPE',
page: '/posts/nextjs_15_migration/opengraph-image.png'
}
そもそも静的書き出し
モードでOGP画像
の生成はドキュメント通りに作るとダメで(Issue
ある)、代わりにOGP画像
を返すRoute Handlers
を作ることで静的書き出し
時にHTML
とともに画像が生成されるようになります。
そのRoute Handlers
機能も本当は静的書き出し
モードでは利用できないのですが、条件を満たした(GET
リクエストのみ + Request
の引数を使わない)場合は静的書き出し時に一緒に呼び出され書き出されるようです。
本題に戻して、OGP
画像にSVG
を<img>
経由で表示しているのですが、これがなんかエラーになってしまっている。
かなり端折っていますが、こんな感じに<img>
でSVG
を表示させてた。<img>
を経由してたのはpublic/
にあるアイコンを読み出したく、ファイルパスより直接渡したほうが良いらしいので。
// 本当は opengraph-image.tsx を作って使います
// ImageResponse を返すことで OGP 画像を返せる Route Handlers が作れる。静的書き出し時はこっちで作る必要がある
export async function GET ( _ : Request , props : PageProps ) {
const params = await props . params ;
return new ImageResponse (
(
// 背景
< div
style ={{
height : '100%' ,
width : '100%' ,
position : 'relative' ,
display : 'flex' ,
backgroundColor : '#fff'
}}
>
{ /* SVG は https://fonts.google.com/icons から。ありざいす */ }
< img
width ={ 50 }
height ={ 50 }
src ={
`data:image/svg+xml;,<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>`
}
/>
</div>
),
{
width: 1200 ,
height: 630
}
)
}
マジでエラーが分からんのでNext.js
がOGP画像
生成で使ってるライブラリvercel/satori
のエラーを出してる箇所を見に行ったところ、
data:image/svg+xml;,
だと間違いで、これでは正規表現が一致しなくて、追加でエンコーディングが何なのかをいれる必要がありました。
なので正解は→ data:image/svg+xml;charset=utf8,{SVGのxmlをここに貼る}
https://github.com/vercel/satori/blob/57a89ea6b1a4fdf3c273b4f6d4f384fa02cacc5c/src/handler/image.ts#L176
てかGoogle
で調べたらutf-8
まで入れてたわ。
ちなみにこのdata:
から始まるやつ、Data URI (URL ?)
とかいう名前がついているらしい。
export async function GET ( _ : Request , props : PageProps ) {
const params = await props . params ;
return new ImageResponse (
(
// 背景
< div
style = { {
height: '100%' ,
width: '100%' ,
position: 'relative' ,
display: 'flex' ,
backgroundColor: '#fff'
} }
>
{ /* SVG は https://fonts.google.com/icons から。ありざいす */ }
< img
width = { 50 }
height = { 50 }
src = {
`data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>`
}
/>
</ div >
),
{
width: 1200 ,
height: 630
}
)
}
playground
で試したい方→ https://og-playground.vercel.app/
because it took more than 60 seconds. Retrying again shortly.
Failed
って出てビビったけど、しばらくした後にリトライしてくれるらしい。
Failed to build /posts/page/[page]/page: /posts/page/1 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/2 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/3 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/4 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/5 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
ただ、後述しますがGitHub Actions
の環境だとこれがよく出てしまってそうなので、タイムアウトを伸ばすようにしました。。
静的書き出しのタイムアウトを伸ばす
ドキュメントあった。
進捗ない場合はタイムアウトさせて再起動する機能がついたそう。
https://nextjs.org/docs/messages/static-page-generation-timeout
静的書き出しの高度な設定
どうやらNext.js 15
からAdvanced Static Generation Control
という、静的書き出しの並列数等を制御する設定が実験的に追加されたそう。
本当にひどいようならここをいじるしか無い?
https://nextjs.org/blog/next-15#advanced-static-generation-control-experimental
静的書き出し出来た
本番更新
PR
作った、マージします
https://github.com/takusan23/ziyuutyou-next/pull/4
GitHub Actions
が走っています
ローカルと同じエラーが出たけどリトライしたら直った話
ローカルと同じエラーが出てしまった。タイムアウトを伸ばさないから、、、
とりあえず今までのCI
が5分
以内で終わってたので5分
、、、と思ったんですけどさっきのCI
が4分
かかってるので2倍にした。
/** @type {import('next').NextConfig} */
module . exports = {
output: 'export' ,
// 省略...
staticPageGenerationTimeout: 60 * 10 // 10分
}
で、伸ばした後リトライしたら逆にいつもの時間で終わった、どういうことなの
でも何回かGitHub Actions
やってるけどやっぱりこのエラーが出る時は出てしまう ので伸ばしておくことにしようと思う。
Failed to build /posts/page/[page]/page: /posts/page/1 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/2 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/3 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/4 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/5 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/6 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/7 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/8 (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/4 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/6 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/7 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/8 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/3 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/1 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/5 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
Failed to build /posts/page/[page]/page: /posts/page/2 (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
OGP 画像が一部壊れている
なんでこんな部分的に壊れるの...?
ちなみにエラー。分からん、、
Generating static pages
Failed to load dynamic font for 自由帳経動直 . Error: [TypeError: fetch failed] {
[cause]: [AggregateError: ] { code: 'ETIMEDOUT' }
}
Failed to load dynamic font for 自由帳国内版入 . Error: [TypeError: fetch failed] {
[cause]: [AggregateError: ] { code: 'ETIMEDOUT' }
}
Failed to load dynamic font for 自由帳作直際大変 . Error: [TypeError: fetch failed] {
[cause]: [AggregateError: ] { code: 'ETIMEDOUT' }
}
Generating static pages
なんかまたタイムアウト系なのでもう一回試したら動いた。
なんかすごい静的書き出しモード
の調子悪い? →うそです。OGP画像
の件に関しては私が悪かった です。
Generating static pages
Generating static pages
✓ Generating static pages
Finalizing page optimization ...
Collecting build traces ...
Exporting (0/3) ...
✓ Exporting (3/3)
これ次回以降もなにかの拍子に失敗する可能性があるってこと?
どうしたものか
これに関しては私が悪かったです 、原因はフォントファイルがWeb 向けの軽量版 を使ってて、必要な文字が網羅されていないのが原因だった、
今まで気付かなかった。。。全部入りの方を使うようにしました。
Next.js
のOGP画像
を作る機能はvercel/og
を使っていて、そのライブラリはvercel/satori
を使っています。
なんでvercel/satori
を直接使っていないのか、vercel/og
は何なのかはこちらにまとまっていました。
ざっくり、フォントをGoogle Fonts
からダウンロードする機能、satori
が吐き出したSVG
をpng
にする等をvercel/og
側でやっているらしい。
https://t28.dev/blog/vercel-og-or-satori-for-me
話を戻して、じゃあWindows
マシンで成功してGitHub Actions
で失敗してんのはなんでだよって話ですが、おそらくこれです。
そのフォントを指定しない場合(もしくはフォントファイルに文字がない場合)はGoogle Fonts
からダウンロードするのですが、Windows
はいい感じにIPv4
で繋いでくれたけどGitHub Actions(というか Linux)
だとおま環で繋がらないIPv6
の方を使ってしまってるのが原因の可能性がある。
いきなり話が広がりすぎやろがいって話ですが、WSL 2(Ubuntu)
でビルドする際にIPv4
を優先するオプションを付けるとだいぶエラーの数が減ったので可能性がある。減っただけでエラー自体はある 。
こんな感じにnode
のオプションをつければ良いはず?
NODE_OPTIONS = '--dns-result-order=ipv4first' npm run build
もし私みたいな事になってしまったら、Web 向けの軽量版フォント
を使うのを辞めるか、
Google Fonts
をあらかじめローカルに落としておいて、satori
にフォントファイルとしてArrayBuffer
で渡すなどすれば良いのかな。
ちな上記のオプションIPv4
を強制するものじゃないのでIPv6
を使われたらやっぱりエラーになってしまう。
これ一時的に IPv6
がダメだったり、Node.js
のバージョン が関与してたり(?)でかなり複雑そう雰囲気。。。
おわりに
ちなみにReact
側の修正は ないはずです。 私の場合はありませんでした
おわりに2
React 19
のuse() Hook
と<Suspense>
でuseEffect()
を消し飛ばしたい
ダークモード切り替えに端末の設定に従うを追加したい
タグ詳細のページネーション
next.config.ts
。TypeScript
で書けるようになった
とか
やりたいんですが、あんまり決まってないのでとりあえずNext.js 15
に上げただけのPR
を書こうと思います。
Tailwind CSS
も次のバージョンが控えてるそうなので今じゃなくていいか。
フロントエンド、むずかしい。
おわりに3
せいてきがえっちな方の漢字になってないかgrep
したけど大丈夫そう