たくさんの自由帳

Next.js の AppRouter に移行する

投稿日 : | 0 日前

文字数(だいたい) : 9960

どうもこんばんわ。 恋にはあまえが必要です 攻略しました。
ゆびさきのときとは違ってちゃんとメッセージアプリのテキストも読み上げてくれます神。

この子のBルートの最後のイベントCGがめっちゃ好みです。

Imgur

Imgur

個人的には 満留さん と 千羽ちゃん のルートが好きです、
Aルートのほうが好みでした。

Imgur

Imgur

かわいい

Imgur

↑ ヒロイン視点がめっちゃいい

Imgur

ルート選択、妹ちゃんルートはちゃんと午後からしか出現しないようになってた(それはそうか

Imgur

あつい・・・あついね
予想よりもめっちゃよかったです!!おすすめ(様子見しようかと思ってたけど予約してよかった)

あとめっちゃ関係ないですがMisskeyのお一人様インスタンス立ててみました。しばらく見てないうちにインスタンスではなくサーバーって言うようになったらしい。
こちらです。立ててしまった以上使わないとお金かかかるので使います・・・多分(???)

なぜか私の鯖からリモートのユーザー情報が取れない鯖があるんですけどよく分かりません・・・
io鯖とかは普通にリモートフォロー出来たのでほんとに謎です・・・

本題

Next.jsAppRouterに移行しようと思います。いい加減やります。
おそらく日が経ったので、PromiseでJSXを返してもエラーにならないはず・・・!

環境

Next.js13.4.4
React18.2.0
TypeScript5.1.3 (後述)
リポジトリapp_router ブランチ

Next.js の AppRouter

React Server Componentsが採用されているので、デフォルトで.tsxを作った場合はサーバー側で描画されます。

???

サーバー側(このブログはSSGですが)でReact (JSX)からHTMLを作ってクライアントに返そう言っています。?
動きのない(タイトルを表示している部分など)はこれを使うと、追加のJavaScriptが無いので軽くなるとか??

サーバー側で描画するため、直接データベース / APIへアクセスし、コンポーネントをクライアントへ返すことができるらしい。うーん難しい
サーバー側で描画されるので、useStateや、onClickContextは使えない(動的な要素が必要ならクライアントコンポーネントが必要)

てなことがここに書いてある。
https://nextjs.org/docs/getting-started/react-essentials#when-to-use-server-and-client-components

データ取得の新しい考え方?

getStaticPropsは無くなったそう。かわりにコンポーネントが非同期で返せる(!?)ので、直接awaitでブログ記事とかを読み込めばいいらしい。
コンポーネントでAPI叩くとか非同期なことできるようになったのがRSCなのか・・・?

上に関連してなんですが、どうやらページの作り方の考え方も変わってるそうで、
今まではgetStaticPropsでデータを取ったら、Propsを他のコンポーネントにバケツリレーしてたと思うのですが、、
AppRouterではできる限り、非同期コンポーネント内でそれぞれgetStaticPropsに当たる取得処理をするのが良いらしい。どゆこと?

// これより ----
export default async function DetailPage() {
    const data = await getArticle() // 記事を読み込む関数 Promise
    return (
        <>
            <Title>{data.title}</Title>
            <Detail body={data.html} />
        </>
    )
}
 
// こっちの方がいいらしい ----
async function Title() {
    const data = await getArticle()
    return (<h1>{data.title}</h1>)
}
async function Detail() {
    const data = await getArticle()
    return (<body dangerouslySetInnerHTML={data.html} />)
}
 
export default async function DetailPage() {
  return (
    <>
        <Title/>
        <Detail />
    </>
  )
}

Material-UI

このブログはMaterial-UIを使っていて、Material-UIはクライアントコンポーネントで描画する必要があります。
というわけで、"use client";をひたすら書いていくのが今回の移行作業だと思います・・・

せっかくApp Routerなのに、全部に全部"use client"したら意味がないんじゃないかと思っていましたが、
サーバーコンポーネント以外にもメリットがあると言ってくれているので、pagesから移行したほうが良さそう
( 非同期なコンポーネントや、Next.js 用 fetch APIpages ディレクトリよりも柔軟なファイル構成 など。特に最後のやつはそれだけでも旨味ありそう? )

移行します

公式
https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration

手順としては・・・

  • もろもろ更新する
    • 暗黙的なchildrenが無くなったりとめんどいかも
  • pagesのルーティングをappに移動する
  • 既存のpagesで動いてたページをクライアントコンポーネントとして使えるようにする("use client"をひたすら付ける)
  • appへサーバーコンポーネントとして動くページを作り、データ取得と↑で作ったクライアントコンポーネントを呼び出す
  • generateStaticParamsを使って動的ルーティングを実装する
  • GoogleAnalyticsグローバルCSSを動くように調節する

がんばりましょう。。。

更新

Next.jsを更新します

npm install next@latest react@latest react-dom@latest 

TypeScriptだけはnpm i -D typescriptで更新されなかったので直接package.jsonをいじりました。
typescript5.1.3以降へ、@types/react18.2.8以降にします。
直接いじった場合、package-lock.jsonnode_moduleを消してnpm iしないとだめです。

  "devDependencies": {
    "@types/react": "^18.2.8",
    "typescript": "^5.1.3"
  }
}

app フォルダを作る

pagesのようにappを作りました。

_app.tsx を layout.tsx にする

_app.tsxを使って共通レイアウトを作ってましたが、App Routerではlayout.tsxを作ることで共通レイアウトを作れるようになりました。

Imgur

app/layout.tsx

import ClientLayout from "./ClientLayout"
 
/** 共通レイアウト部分 */
export default function RootLayout({ children, }: { children: React.ReactNode }) {
    // クライアントコンポーネントとして描画する必要があるため
    return (<ClientLayout children={children} />)
}

app/ClientLayout.tsx

// Material-UI を使うためクライアントコンポーネント
"use client"
 
import { ThemeProvider } from '@mui/material/styles'
import Layout from '../../components/Layout'
import useCustomTheme from '../../src/ZiyuutyouTheme'
import { useEffect, useState } from "react"
import useMediaQuery from '@mui/material/useMediaQuery'
 
/** ClientLayout へ渡す値  */
type ClientLayoutProps = {
    /** 子要素 */
    children: React.ReactNode
}
 
/** 共通レイアウト */
export default function ClientLayout({ children }: ClientLayoutProps) {
    // ダークモードスイッチ
    const [isDarkmode, setDarkmode] = useState(false)
    // テーマ。カスタムフック?何もわからん
    const theme = useCustomTheme(isDarkmode)
    // システム設定がダークモードならダークモードにする。Win10で確認済み
    const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)')
    // システム設定のダークモード切り替え時にテーマも切り替え
    useEffect(() => {
        setDarkmode(prefersDarkMode)
    }, [prefersDarkMode])
 
    return (
        <>
            <ThemeProvider theme={theme}>
                {/* ナビゲーションドロワーとタイトルバーをAppで描画する。各Pageでは描画しない */}
                <Layout
                    isDarkmode={isDarkmode}
                    onDarkmodeChange={() => setDarkmode(!isDarkmode)}
                >
                    {/* 各Pageはここで切り替える。これでタイトルバー等は共通化される */}
                    {children}
                </Layout>
            </ThemeProvider>
        </>
    )
}

_document.tsx を layout.tsx にする

また、next/headも変更されており、メタデータを公開するような形になっています。
_document.tsxheadいじってた場合はここに書くっぽい。

pages/_document.tsx

export default class Document extends NextDocument {
    render() {
        return (
            <Html>
                <Head>
                    {/* PWA */}
                    <link rel="icon" sizes="192x192" href="/icon.png" />
                    <link rel="manifest" href="/manifest.json" />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

app/layout.tsx

import { Metadata } from "next"
import ClientLayout from "./ClientLayout"
 
export const metadata: Metadata = {
    manifest: '/manifest.json'
}
 
/** 共通レイアウト部分 */
export default function RootLayout({ children, }: { children: React.ReactNode }) {
    // TODO Google Analytics
    // クライアントコンポーネントとして描画する必要があるため
    return (<ClientLayout children={children} />)
}

iconfaviconは、appフォルダ内に入れておくことで自動で認識して追加してくれるそうです。
Imgur

https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons#image-files-ico-jpg-png

pages を app に移動する

pages/index.tsxをクライアントコンポーネントにします。

app/ClientHomePage.tsx

// Material-UI を使うため クライアントコンポーネント
"use client"
 
import LinkCard from "../../components/LinkCard";
import MakingAppCard from "../../components/MakingAppCard";
import ProfileCard from "../../components/ProfileCard";
import Spacer from "../../components/Spacer";
import LinkData from "../../src/data/LinkData";
import { MakingAppData } from "../../src/data/MakingAppData";
 
/** ClientHomePage へ渡すデータ */
type ClientHomePageProps = {
    /** ランダムメッセージの配列 */
    randomMessageList: Array<string>,
    /** 作ったアプリ配列 */
    makingAppList: MakingAppData[],
    /** リンク集 */
    linkList: LinkData[]
}
 
/** 最初に表示する画面 */
export default function ClientHomePage(props: ClientHomePageProps) {
    return (
        <>
            <ProfileCard randomMessageList={props.randomMessageList} />
            <Spacer value={1} />
            <LinkCard linkList={props.linkList} />
            <Spacer value={1} />
            <MakingAppCard makingAppList={props.makingAppList} />
        </>
    )
}

次に、app/page.tsxを作り、getStaticPropsでやっていたロード処理を非同期コンポーネント内でやるようにします。

app/page.tsx

import { Metadata } from "next";
import JsonFolderManager from "../src/JsonFolderManager";
import ClientHomePage from "./ClientHomePage";
 
/** <head> に入れる値 */
export const metadata: Metadata = {
    title: 'トップページ - たくさんの自由帳'
}
 
/** 最初に表示されるページ */
export default async function Home() {
    // データを async/await を使って取得する
    // なんとなく並列にしてみた
    const [randomMessageList, makingAppList, linkList] = await Promise.all([
        // ランダムメッセージ
        JsonFolderManager.getRandomMessageList(),
        // 作ったアプリ
        JsonFolderManager.getMakingAppMap(),
        // リンク集
        JsonFolderManager.getLinkList()
    ])
 
    return (<ClientHomePage randomMessageList={randomMessageList} makingAppList={makingAppList} linkList={linkList} />)
}

なんとなく並列にしてみただけで、大人しく一個一個awaitしても問題ないはずです。

// これでもいい
// ランダムメッセージ
const randomMessageList = await JsonFolderManager.getRandomMessageList()
// 作ったアプリ
const makingLovers = await JsonFolderManager.getMakingAppMap()
// リンク集
const linkList = await JsonFolderManager.getLinkList()

最後に、pages/index.tsxpages/_app.tsxpages/_document.tsxを消します。残しておくと、appなのかpagesなのかどっちなんだい!ってなっちゃうので

どうだろう、これで見れるはず?
Imgur

ひたすら pages を app にする作業をする

これを繰り返します。
getStaticPathsは後でやります。

ルーティング

App Routerは、フォルダを作っただけではパスとしては認識されません。
フォルダの中にpage.tsxがあるかでパスとしては認識するかどうかが決まります。

なのでpagesでこうだった構成だと

  • pages
    • pages
      • [page].tsx
    • posts
      • page
        • [page].tsx
      • tag
        • [tag].tsx
        • all_tags.tsx
      • [blog].tsx

appだとこうなるはず

  • app
    • pages
      • [page]
        • page.tsx
    • posts
      • [blog]
        • page.tsx
      • page
        • [page]
          • page.tsx
      • tag
        • [tag]
          • page.tsx
        • all_tags
          • page.tsx

パスの名前のフォルダを作る必要がある感じですね。
一見ややこしいように見えますが、page.tsxを置いてないフォルダはさっきの通りパスとしては認識されないので、
同じ場所にテストコード、コンポーネントを置くなどができるようになりました。

  • app
    • posts
      • posts-components // 記事表示で使うコンポーネント置き場
        • Title.tsx
      • tests // テストコード
        • test.tsx
      • [blog]
        • page.tsx // 記事表示

わかりやすいような・・・ややこしいような・・・

あと動的ルーティングはフォルダ名を[動的ルーティング名]にすればいいです。([id]とか[page]とか)
page.tsxからは以下のように引数で動的ルーティングのパス名が取れるようになります。

// slug は [slug] だから。
// フォルダ名が [id] なら { id: string } が正解
export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

どうでしょう、CSSが当たってなかったりしますが、、、、とりあえずは出るようになりましたか?

Imgur

もしConflicting app and page file foundがでてしまったら、一度開発サーバーを起動し直すといいかもしれないです。
npm run dev

動的ルーティング を返してあげる

動的ルーティングで生成されるパス一覧を返してあげます。
getStaticPathsAppRouterではgenerateStaticParamsになります。
APIはそんなに変わってないはずです。

pages/posts/[blog].tsx

/**
 * ここで生成するページを列挙して返す。(実際にはパスの一部)
 * 
 * /posts/<ここ> ←ここの部分の名前を渡して生成すべきページを全部列挙して返してる
 * 
 * これも上記同様クライアント側では呼ばれない。
 */
export const getStaticPaths: GetStaticPaths = async () => {
    const fileNameList = (await ContentFolderManager.getBlogNameList())
        // この場合はキーが blog になるけどこれはファイル名によって変わる([page].tsxなら page がキーになる)
        .map(name => ({ params: { blog: name } }))
    return {
        paths: fileNameList,
        fallback: false
    }
}

app/posts/[blog]/page.tsx

/**
 * ここで生成するページを列挙して返す。(実際にはパスの一部)
 * 
 * /posts/<ここ> ←ここの部分の名前を渡して生成すべきページを全部列挙して返してる
 * 
 * これも上記同様クライアント側では呼ばれない。
 */
export async function generateStaticParams() {
    const fileNameList = await ContentFolderManager.getBlogNameList()
    // この場合はキーが blog になるけどこれはファイル名によって変わる([page].tsxなら page がキーになる)
    return fileNameList.map((name) => ({ blog: name }))
}

404.tsx を not-found.tsx へ移行する

pages/404.tsxapp/not-found.tsxへ移動します。
これでpagesフォルダーは削除できるようになるはず・・・!

CSS をなおす

layout.tsxcssを読み込むことが出来ます

import { Metadata } from "next"
import ClientLayout from "./ClientLayout"
// コードブロックのCSS
import "highlight.js/styles/vs2015.css"
// グローバルCSS
import "../styles/css/global.css"

GoogleAnalytics をなおす

これはGA4SPAでも使える設定(なんだっけ?、パスの変化を検知するみたいなやつ)をしていない場合に必要です。
useRouterで遷移時にイベントを取得し、そのタイミングでGAの遷移イベントを飛ばしていたやつは修正が必要です。

こーゆーやつ ↓↓↓

useEffect(() => {
    const handleRouteChange = (url: string) => {
        pageview(url)
    }
    router.events.on('routeChangeComplete', handleRouteChange)
    return () => {
        router.events.off('routeChangeComplete', handleRouteChange)
    }
}, [router.events])

で、useRouter usePathname useSearchParamsの3つに分裂したそうなので置き換えます。
クライアントコンポーネントである必要があります。

https://nextjs.org/docs/app/api-reference/functions/use-router#router-events

/** Google Analytics 4 で利用するJavaScriptを差し込むやつ。本番(意味深)のみ実行 */
export default function GoogleAnalytics() {
    const pathname = usePathname()
    const searchParams = useSearchParams()
 
    // Google Analytics へnext/routerのページ遷移の状態を通知する
    useEffect(() => {
        const url = `${pathname}${searchParams}`
        pageview(url)
    }, [pathname, searchParams])
 
    // 本番ビルド時のみ GoogleAnalytics をセットアップする
    return (
        <>
            {!isDevelopment && <>
                <Script
                    strategy="afterInteractive"
                    src={`https://www.googletagmanager.com/gtag/js?id=${UA_TRACKING_ID}`}
                />
                <Script
                    strategy="afterInteractive"
                    dangerouslySetInnerHTML={{
                        __html: `
                        window.dataLayer = window.dataLayer || [];
                        function gtag(){dataLayer.push(arguments);}
                        gtag('js', new Date());
                        gtag('config', '${UA_TRACKING_ID}');
                        gtag('config', '${GA_TRACKING_ID}');
                    `}}
                />
            </>}
        </>
    )
}

app/layout.tsxで呼び出せば良いのですが、<suspense>でくくらないと怒られます。

/** 共通レイアウト部分 */
export default function RootLayout({ children, }: { children: React.ReactNode }) {
    // クライアントコンポーネントとして描画する必要があるため
    return (
        <html>
            <body className={koruriFont.variable}>
                {/* 共通レイアウト。ナビゲーションドロワーとか */}
                <ClientLayout children={children} />
                {/* GoogleAnalytics */}
                <Suspense fallback={null}>
                    <GoogleAnalytics />
                </Suspense>
            </body>
        </html>
    )
}

これで一通り出来たかな???

静的サイト書き出しを有効にして、本番ビルドしてみる

https://nextjs.org/docs/pages/building-your-application/deploying/static-exports

next.config.jsに追記する必要があります。

// https://github.com/vercel/next.js/blob/canary/examples/progressive-web-app/next.config.js
const withPWA = require('next-pwa')({
    // https://github.com/GoogleChrome/workbox/issues/1790#issuecomment-729698643
    disable: process.env.NODE_ENV === 'development',
    dest: 'public',
})
 
module.exports = withPWA({
    output: 'export', // これ
    trailingSlash: true,
    experimental: {
        scrollRestoration: true,
    }
})

また、output: 'export'が追加された影響で、npx next exportが無くなったため、package.jsonに書いたビルドコマンドも修正する必要があります。(npx next buildだけでよくなりました)

- "deploy": "npm run build && npm run export && npm run postbuild"
+ "deploy": "npm run build && npm run postbuild"

これでビルドしてみる。多分動くはず。
流石に全部やると時間かかりすぎるので記事を2個ぐらいにした。

型チェックが厳しくなった?

型のチェックが厳しくなっている?

./components/MakingAppCard.tsx:120:20
Type error: Object is possibly 'undefined'.
 
  118 |     const changeAppListPlatform = (platformName: string) => {
  119 |         // あれTypeScriptくんこれ通すんか?
> 120 |         setAppList(makingAppList.find(platformObj => platformObj.platfromName === platformName).appList)
      |                    ^
  121 |     }
  122 | 
  123 |     /**
const makingApp = makingAppList.find(platformObj => platformObj.platfromName === platformName)
if (makingApp) {
    setAppList(makingApp.appList)
}

TypeScript、エラーがわかりにくい気がするんですけど、私だけなんですかね・・・

./src/MarkdownParser.ts:97:15
Type error: Type '{ label: string | null; level: number; hashTag: string; }[]' is not assignable to type 'TocData[]'.
  Type '{ label: string | null; level: number; hashTag: string; }' is not assignable to type 'TocData'.
    Types of property 'label' are incompatible.
      Type 'string | null' is not assignable to type 'string'.
        Type 'null' is not assignable to type 'string'.

KotlinfilterNotNullに当たる処理が思いつかないので調べたんですが、flatMapを使う方法だと型を予測して解決してくれていい感じ。
https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array

/**
 * HTML を解析して 目次データを作成する。結構時間がかかる。
 * 
 * @param html HTML
 * @returns 目次データの配列
 */
static parseToc(html: string): TocData[] {
    // HTML パーサー ライブラリを利用して h1 , h2 ... を取得する
    // この関数は ブラウザ ではなく Node.js から呼び出されるため、document は使えない。
    const window = (new JSDOM(html)).window
    const document = window.document
    const tocElementList = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
    // 目次データに変換して返す
    const tocDataList: TocData[] = Array.from(tocElementList)
        .map(element => {
            if (element.textContent) {
                return {
                    label: element.textContent,
                    level: Number(element.tagName.charAt(1)), // h1 の 1 だけ取り出し数値型へ
                    hashTag: `#${element.getAttribute('id')}` // id属性 を取り出し、先頭に#をつける
                }
            } else {
                return null
            }
        })
        // null を配列から消す技です
        .flatMap(tocDataOrNull => tocDataOrNull ? [tocDataOrNull] : [])
    window.close()
    return tocDataList
}

静的書き出し結果を見てみる

npx next startは使えなくなりました。

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

かわりに、以下のコマンドで起動できます。

npx serve@latest out

うーんなんか全然動いて無くないか?
/posts/page/1/を押してもなんかパスが中途半端なんですけど?generateStaticParams間違ってました。ごめんなさい

Imgur

ついでに直したいところ

他に直したい部分が何個かあるんですよね...

  • ドメインをハードコートするのをやめて、環境変数にする
  • ドキュメント通りのサイトマップ生成を使う
  • app/README.md 更新する
  • Babel をやめたい
  • ServiceWorker やめたい
  • next/font を利用したい
  • トップページの画像変えたい
  • サイトマップ生成も変えたい
  • 404 の画像変えたい
  • OSSライセンス画面更新
  • ビルドコマンドの修正(ライブラリ無しでサイトマップ生成したのでpostbuildなしでよくなった。npx next buildだけでいいはず)

サイトマップ生成

App Routerから、サイトマップ生成がライブラリ無しで作れるようになったっぽいので、移行します。
https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap

app/sitemap.ts

import { MetadataRoute } from "next";
import UrlTool from "../src/UrlTool";
import ContentFolderManager from "../src/ContentFolderManager";
 
/**
 * サイトマップを生成する。Next.js 単体で作れるようになった。
 * Trailing Slash が有効なので最後にスラッシュ入れました。
 */
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const currentTime = new Date()
 
    // 静的ページ
    const staticPathList: MetadataRoute.Sitemap = [
        {
            url: `${UrlTool.BASE_URL}/`,
            lastModified: currentTime
        }
    ]
 
    // ブログ記事
    const blogPathList = (await ContentFolderManager.getBlogNameList())
        .map(name => ({
            url: `${UrlTool.BASE_URL}/posts/${name}/`,
            lastModified: currentTime
        }))
 
    // 固定ページ
    const pagePathList = (await ContentFolderManager.getPageNameList())
        .map(name => ({
            url: `${UrlTool.BASE_URL}/pages/${name}/`,
            lastModified: currentTime
        }))
 
    return [...staticPathList, ...blogPathList, ...pagePathList]
}

npm run buildしたあとout/sitemap.xmlが出来て、こんなのが出てきます。

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://takusan.negitoro.dev/</loc>
<lastmod>2023-06-18T17:15:15.809Z</lastmod>
</url>
<url>
<loc>https://takusan.negitoro.dev/posts/next_js_13_app_router_migration/</loc>
<lastmod>2023-06-18T17:15:15.809Z</lastmod>
</url>
<url>
<loc>https://takusan.negitoro.dev/posts/windows_winui3_installer_and_virtual_desktop/</loc>
<lastmod>2023-06-18T17:15:15.809Z</lastmod>
</url>
<url>
<loc>https://takusan.negitoro.dev/pages/about/</loc>
<lastmod>2023-06-18T17:15:15.809Z</lastmod>
</url>
</urlset>

Babel をやめて SWC にする

https://nextjs.org/docs/architecture/nextjs-compiler

そもそも何だこれはという話ですが、なんかSWCを使ったほうが高速に動くらしい。
で、SWCにしたいところなんですが、、Babelsvgをコンポーネント化するプラグインを使っているので、この代わりをどうにかする必要があります。

困ってる人私以外にもいた
https://github.com/vercel/next.js/discussions/33161

SVGRを使っている人が多そうかな、じゃあこれで
https://react-svgr.com/docs/next/

.babelrcを消して、プラグインも消しちゃいます。

次に入れて
npm install --save-dev @svgr/webpack

next.config.jsに書き足します。
webpackの箇所ですね。

output: 'export',
trailingSlash: true,
webpack(config) {
    // SVG をコンポーネントにできる
    config.module.rules.push({
        test: /\.svg$/,
        use: ['@svgr/webpack'],
    })
    return config
},
experimental: {
    scrollRestoration: true,
}

あとはsvgのパスをインポートして、JSX書けば良いはず!すごい!

// https://react-svgr.com/docs/next/ 参照
import NotFoundIcon from "../public/not_found.svg"
 
export default function Component() {
    return (
        <>
            <NotFoundIcon
                className={'theme_color'}
                height={250}
                width={250}
            />

next/font を使う

こっちの方がいいらしい。ので
前は@next/fontを入れる必要があったみたいですが、Next.jsに組み込まれたみたいなので、入れなくても使えるらしい。
app/layout.tsxで読み込めば良さそう。

用意したフォントを読み込みたいので、localの方を使うぽい
https://nextjs.org/docs/app/building-your-application/optimizing/fonts#local-fonts

variableを指定すると、.cssなどから参照できるようになります。
<body className={koruriFont.variable}>を忘れないようにしてください。(一敗)

app/layout.tsx

import localFont from "next/font/local"
 
/** フォントを読み込む */
const koruriFont = localFont({
    // CSS 変数として使う
    variable: '--koruri-font',
    src: [
        { path: '../styles/css/fonts/Koruri-Regular-sub.eot' },
        { path: '../styles/css/fonts/Koruri-Regular-sub.ttf' },
        { path: '../styles/css/fonts/Koruri-Regular-sub.woff' },
    ]
})
 
/** 共通レイアウト部分 */
export default function RootLayout({ children, }: { children: React.ReactNode }) {
    // クライアントコンポーネントとして描画する必要があるため
    return (
        <html>
            <body className={koruriFont.variable}>
                {/* 共通レイアウト。ナビゲーションドロワーとか */}
                <ClientLayout children={children} />
                {/* GoogleAnalytics */}
                <Suspense fallback={null}>
                    <GoogleAnalytics />
                </Suspense>
            </body>
        </html>
    )
}

CSSからはvar()で参照できます。
global.css

/* コードブロック */
code {
    overflow-x: scroll;
    font-family: var(--koruri-font);
    background-color: rgba(0, 0, 0, 0.05);
}

Material-UIcreateThemeも多分これで参照できているはず。

createTheme({
    typography: {
        fontFamily: [
            'var(--koruri-font)'
        ].join(','),
    }
})

はい
Imgur

next/head が無くなってしまったので、クライアントコンポーネントで head が操作できない?

唯一困ったかもしれない。
next/headを使って、テーマの色に合わせてAndroidのステータスバーの色も同じ色に合わせるようにしていたのですが、AppRouterで使えなくなってしまった。
とりあえずDOMJavaScriptで無理やり変えてるんですけど、、、これでいいの?

"use client"
 
import { useEffect, useRef } from "react"
 
/**
 * Androidのステータスバーに色を設定する
 * AppRouter だと next/head が使えない。多分現状クライアントコンポーネントで head の中身を変える方法が Next.js にはなさそう?
 * 仕方ないので、DOM を触る。
 * 
 * @param colorCode カラーコード
 */
export default function useAndroidStatusBarColor(colorCode: string) {
    const statusBarColorMeta = useRef<HTMLMetaElement>()
 
    // 追加する
    useEffect(() => {
        statusBarColorMeta.current = document.createElement('meta')
        statusBarColorMeta.current.setAttribute('name', 'theme-color')
        document.head.append(statusBarColorMeta.current)
    }, [])
 
    // 色が変化したら更新する
    useEffect(() => {
        statusBarColorMeta.current?.setAttribute('content', colorCode)
    }, [colorCode])
}

動きました!

pagesapp差分はこんな感じになります。

https://github.com/takusan23/ziyuutyou-next/pull/1

実際に適当に公開しても問題なさそうだったので、人がいなさそうな時にあげようかな。いや別にいつでも良いか・・?

2023/07/11 午前2~3時 くらいに入れました

Next.js AppRouter 対応を入れます。
見てる人無さそうだったので(そもそもあんまり居ない;;

おわりに

めちゃ関係ないけどTailwind CSSチョットだけ触ってみましたが、これで良くない?
サーバーコンポーネントとしてもTailwind CSSは使えるみたいなのでMaterial-UIをやめてもいいかもしれない

あと公式ドキュメント、Chromeの翻訳してるとルーティング失敗しない?
あとVSCodeAltキーを押しながらスクロールすると高速でスクロールできます。

おわりに2

ストレージがたりない!!!

[Error: ENOSPC: no space left on device, write]