mythfinder

📝

ブログをNext.js 13に移行した

(2023/10/05 追記)Astroで書き直した

はじめに

🎉 Next.js 13.2がリリースされ、App Router (beta) & React Server Component(以下RSC)が実用できそうな感じになってきたため、このサイトでも移行した。Metadata API、Route Handler等も同時に導入した。

GitHub - haxibami/haxibami.net: haxibami's website. haxibami's website. Contribute to haxibami/haxibami.net development by creating an account on GitHub. github.com
GitHub - haxibami/haxibami.net: haxibami's website.

やったこと

(一般的な設定についてはドキュメント等を参照)

next.config.js

next.config.mjs
/** @type {import('next').NextConfig} */
let nextConfig = {
  reactStrictMode: true,
  experimental: {
    serverComponentsExternalPackages: [
      "playwright",
      "svgo",
      "plaiceholder",
      "@plaiceholder/next",
      "fetch-site-metadata",
    ],
    scrollRestoration: true,
    appDir: true,
  },
  images: {
    formats: ["image/avif", "image/webp"],
    domains: ["asciinema.org", "raw.githubusercontent.com"],
  },
};

experimental.serverComponentsExternalPackagesという項目がけっこう重要。Markdownの処理にNode.jsのライブラリを使用している場合、該当ライブラリをここに列挙する必要がある。

next.config.js Options: serverComponentsExternalPackages | Next.js Opt-out specific dependencies from the Server Components bundling and use native Node.js `require`. nextjs.org
next.config.js Options: serverComponentsExternalPackages | Next.js

あと関係ないがnext.config.jsはドシドシESM(.mjs)で書こう。

Markdown / MDXの処理

公式ブログを見る限り、@next/mdxnext-mdx-remotecontentlayerの三者は現時点(2023/03/03)でRSCに対応している。

GitHub - hashicorp/next-mdx-remote: Load mdx content from anywhere through getStaticProps in next.js Load mdx content from anywhere through getStaticProps in next.js - hashicorp/next-mdx-remote github.com
GitHub - hashicorp/next-mdx-remote: Load mdx content from anywhere through getStaticProps in next.js

Contentlayer makes content easy for developers Contentlayer is a content SDK that validates and transforms your content into type-safe JSON data you can easily import into your application. contentlayer.dev
Contentlayer makes content easy for developers

next-mdx-remote/rscを使ったところ、入出力の仕様が微妙に変更されていて戸惑った。compileMDXでJSXを直接吐かせるか、<MDXRemote>コンポーネントにMarkdown / MDXを食わせるか選べるようだ(おそらくどちらでも出力に差はない)。

そのほか、frontmatterに型を付けられるようになった。

lib/compiler.ts
import { compileMDX } from "next-mdx-remote/rsc";
...
import MDXComponent from "components/MDXComponent";
...
const compiler = async (source: string) => {
  const result: Promise<{
    content: JSX.Element;
    frontmatter: {
      slug: string;
      title: string;
      date: string;
      description: string;
      tags: string[];
    };
  }> = compileMDX({
    source,
    components: MDXComponent,
    options: {
      ...
    },
  });
  return result;
};

export default compiler;

Route HandlerによるOpen Graph(OG)画像生成

Next.js 13.2で従来のAPI Routesを代替するRoute Handlerが登場したため、ついにOG 画像生成で使っていたpagesディレクトリを完全に廃止1できるようになった。

Routing: Route Handlers | Next.js Create custom request handlers for a given route using the Web's Request and Response APIs. nextjs.org
Routing: Route Handlers | Next.js

Metadata API

同じくNext.js 13.2で登場。サイトマップの生成に使った。

src/app/sitemap.ts
import type { MetadataRoute } from "next";

import { globby } from "globby";

import { dateConverter } from "lib/build";
import { HOST } from "lib/constant";
// Article index file
import postIndex from "share/index.json";

import type { PostData } from "lib/interface";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const constPaths = await globby(["src/app/**/page.tsx", "src/app/page.tsx"], {
    ignore: ["src/app/api/*.tsx", "src/app/grad_essay/**", "src/app/**/[*/**"],
  });

  const constPageEntries = constPaths.map((filePath) => {
    const constPageEntry = {
      relpath: filePath.replace("src/app/", "").replace("page.tsx", ""),
      lastmod: "",
    };
    return constPageEntry;
  });

  const blogposts = postIndex.articles.blog;

  const blogTags = postIndex.tags.blog;

  const blogEntries = blogposts.map((post: PostData) => {
    const blogEntry = {
      relpath: `blog/posts/${post.data?.slug}`,
      lastmod: dateConverter(post.data?.date),
    };
    return blogEntry;
  });

  const blogTagEntries = blogTags.map((tag: string) => {
    const blogTagEntry = {
      relpath: `blog/tag/${tag}`,
      lastmod: "",
    };
    return blogTagEntry;
  });

  const sitemapEntries = constPageEntries.concat(blogEntries, blogTagEntries);
  return sitemapEntries.map((entry) =>
    entry.lastmod !== ""
      ? {
          url: `https://${HOST}/${entry.relpath}`,
          lastModified: entry.lastmod,
        }
      : {
          url: `https://${HOST}/${entry.relpath}`,
        },
  );
}

所感

もともと完全な静的サイトなので、実のところそれほどパフォーマンスに変化はない。バンドルサイズは多少小さくなったかもしれないが。

ビルド結果
ビルド結果

ちなみにLighthouseのスコアはこんな感じ:

トップ
トップ
プロフィール
プロフィール
この記事
この記事

脚注

  1. 厳密には404.jsがまだ残っているが、こちらで書かなくても処理されるのでpagesディレクトリ自体は削除可能