Next.jsの静的サイトをAstroに移行する手順

投稿日 2023-07-23
最終更新日 2023-11-18

先日ブログをNext.jsからAstroに移行しました。
せっかくなのでその移行手順をまとめてみることにしました。

コード例

今回は、以下の [id].tsx[id].astro に移行していこうと思います。
ブログ記事を取得してその内容を表示するページになります。getStaticPathでblogIdに応じて動的にルーティングし、その値に応じて対応するブログ記事を返します。
CSSフレームワークとしてTailwindCSSを採用しています。

import Head from "next/head";
import Layout from "../../components/Layout";
import Link from "next/link";
import { getAllPostIds, getPostData } from "../../lib/post";
import { PostData } from "../../types/postData";
import dayjs from "dayjs";
import React from "react";

export const getStaticPaths = async () => {
  const paths = await getAllPostIds();
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps = async (params: { params: { id: string } }) => {
  const postData: PostData = await getPostData(params.params.id);

  return {
    props: { postData },
  };
}

export default function Post({ postData }: { postData: PostData }) {
  const _blogContent = postData.blogContentHTML;
  let blogContent = "<div>Not founded Blog Content</div>";
  if (_blogContent !== undefined) {
    blogContent = _blogContent;
  }
  return (
    <Layout>
      <div className="md:mt-10 px-4 md:px-24">
        <Head>
          <title>{postData.title}</title>
        </Head>
        <article className="flex flex-col m-auto max-w-screen-md justify-center">
          <div className="flex flex-col space-y-1 md:space-y-2">
            <h1 className="text-xl md:text-4xl mt-10 text-left">
              {postData.title}
            </h1>
            <div className="flex text-xs text-giray/50 space-x-2 justify-left">
              <div>
                投稿日 {dayjs(postData.publishedAt).format("YYYY-MM-DD")}
              </div>
              <div>
                最終更新日 {dayjs(postData.updatedAt).format("YYYY-MM-DD")}
              </div>
            </div>
          </div>
          <div
            className="prose md:prose-xl text-xs md:text-xl py-10 w-full markdown"
            dangerouslySetInnerHTML={{ __html: blogContent }}
          />
          <div className="md:my-10">
            <Link
              href={`/page/${postData.pageId}`}
              className="text-xs md:text-xl hover:cursor hover:text-blue-300"
            >
              トップページへ戻る
            </Link>
          </div>
        </article>
      </div>
    </Layout>
  );
}


早速移行していきましょう。

移行手順

Step0 : Astroプロジェクトを作成する

これがないと始まりません。
npm create astro@latest で始めましょう。
今回は.tsx を移行するのでTypeScriptを有効にします。
また、TailwindCSSの導入は以下を参考にしましょう。(かなり簡単です)
(https://docs.astro.build/ja/guides/integrations-guide/tailwind/)
プロジェクトの作成が終わり次第、既存のNext.jsのプロジェクトフォルダを編集しやすい場所に移動しておきます。
私は、.gitignore に含めることを条件にAstroプロジェクト直下に置いてしまいました。

Step0.5 : Migration Guideを開く

Astro公式のMigration Guideがあります。 → https://docs.astro.build/ja/guides/migrate-to-astro/from-nextjs/
非常に重要です。絶対に見ましょう。
私も、基本的にはここに書いてあることをベースに修正を行いました。

Step1 : コードをそのままコピペする。

src/pages/post フォルダ直下に[id].astro を作成し、[id].tsx をそのままコピペします。
当然大量のエラーが出るためこれから直していきましょう。

Step2 : コードフェンスを作成し、関数コンポーネントのreturn文以外を移動

今回の場合、import文、関数コンポーネント以外の関数、関数コンポーネントのJSXではない部分を全てコードフェンスに移動してしまいます。
また、 コードフェンス外の部分はHTMLで表現するのでreturnで返されるJSXは外に出してしまいましょう。
結果以下のようになるはずです。

---
import Head from "next/head";
import Layout from "../../components/Layout";
import Link from "next/link";
import { getAllPostIds, getPostData } from "../../lib/post";
import { PostData } from "../../types/postData";
import dayjs from "dayjs";
import React from "react";

export const getStaticPaths = async () => {
  const paths = await getAllPostIds();
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps = async (params: { params: { id: string } }) => {
  const postData: PostData = await getPostData(params.params.id);

  return {
    props: { postData },
  };
};

const _blogContent = postData.blogContentHTML;
  let blogContent = "<div>Not founded Blog Content</div>";
  if (_blogContent !== undefined) {
    blogContent = _blogContent;
  }
---

<Layout>
  <div className="md:mt-10 px-4 md:px-24">
    <Head>
      <title>{postData.title}</title>
    </Head>
    <article className="flex flex-col m-auto max-w-screen-md justify-center">
      <div className="flex flex-col space-y-1 md:space-y-2">
        <h1 className="text-xl md:text-4xl mt-10 text-left">
          {postData.title}
        </h1>
        <div className="flex text-xs text-giray/50 space-x-2 justify-left">
          <div>
            投稿日 {dayjs(postData.publishedAt).format("YYYY-MM-DD")}
          </div>
          <div>
            最終更新日 {dayjs(postData.updatedAt).format("YYYY-MM-DD")}
          </div>
        </div>
      </div>
      <div
        className="prose md:prose-xl text-xs md:text-xl py-10 w-full markdown"
        dangerouslySetInnerHTML={{ __html: blogContent }}
      />
      <div className="md:my-10">
        <Link
          href={`/page/${postData.pageId}`}
          className="text-xs md:text-xl hover:cursor hover:text-blue-300"
        >
          トップページへ戻る
        </Link>
      </div>
    </article>
  </div>
</Layout>


Step3 : インポート文の修正

コードフェンス内のimport文のエラーを解消します。
主に以下の点を修正します。

  • Next専用のコンポーネントは全て削除します。今回の例では、HeadLink 等があたります。
  • 外部パッケージが必要な場合はインストールします。この例では、dayjsが該当します。
  • 自作のコンポーネント(レイアウト)をインポートしている場合は、将来的にそれもAstroに変換します。そのため、一旦空のastroファイルを作ってそれをインポートしておきましょう。今回の例ではLayout が自作のレイアウトにあたるのでsrc/layouts/ 直下にLayout.astroを作成しておきます。拡張子が.astro のため、import文にも明記しておきましょう。
  • その他のインポートエラーは該当ファイル(.ts など)をAstroプロジェクトに移行することで解決できると思います。ただし、型をインポートする場合は、import type にしないとエラーが出るので気をつけましょう。
  • 最後に不要なimport文は削除しておきます。今回はReact が不要なので削除しておきます。


最終的なimport文は以下のようになると思います。

import Layout from "../../layouts/Layout.astro";
import { getAllPostIds, getPostData } from "../../lib/post";
import type { PostData } from "../../types/postData";
import dayjs from "dayjs";


Step4 import文以下のコードフェンスを編集

そのほかの部分も編集していきます。今回は、getStaticPaths と、getStaticProps になります。
getStaticPaths に関しては、同名で同じ挙動の関数があるのでそれを活用していきます。
使い方としては以下になります。

---
export async function getStaticPaths() {
  return [
    { params: { /* required */ }, props: { /* optional */ } },
    { params: { ... } },
    { params: { ... } },
    // ...
  ];
}
---

※ 上記コードはhttps://docs.astro.build/en/reference/api-reference/#getstaticpaths より引用しています。

上の例では任意でビルド時に、propsまで取得するよう書き換えられることがわかりますが、今回はリプレイスのしやすさを考慮してparamsだけ取得しています。
おそらく、Next.jsのGetStaticPathsもparamsプロパティが含まれたオブジェクトの配列となっているため、fallbackを消してやれば動くと思います。
そしてgetStaticPathsによって取得されたパラメータは以下のように書くと取得できます。

const { id } = Astro.params;


次にgetStaticPropsを移行しますが、これに該当する関数はAstroにはないのでコードフェンスに直接書いてしまいます。
先ほど、取得したparamsを利用してプロパティを取得していきましょう。
最終的には以下のようになります。

export const getStaticPaths = async () => {
  const paths = await getAllPostIds();
  return paths
};

const { id } = Astro.props;
const postData: PostData = await getPostData(id!);

const _blogContent = postData.blogContentHTML;
let blogContent = "<div>Not founded Blog Content</div>";
if (_blogContent !== undefined) {
  blogContent = _blogContent;
}


Step5 コンポーネントテンプレート

最後に、コンポーネントテンプレート(コードフェンスの下のHTML)を編集していきます。
具体的には以下のように直します。

  • <Head><head> にします。(そもそもヘッド部はレイアウトコンポーネントで定義した方が良いので消しても良いです)
  • <Link><a> にします。
  • <Image><img> にします。
  • classNameclass にします。

基本的にはHTMLと同じタグの名前に変更すれば大丈夫です。

また、ブログサイトなどの場合、以下のようにdangerouslySetInnerHTMLを使うことがあると思います。

          <div
            dangerouslySetInnerHTML={{ __html }}
          />


あくまでこれはReactのプロパティのため、Astroでは使うことができません。
代わりに、set:htmlで代用します。これは「テンプレートディレクティブ」と呼ばれる記法のようです。

<div set:html={blogContent} />


あくまでdangerouslySetInnerHTMLと同じような機能でありXSSの危険性がつきまとうので慎重に扱いましょう。
詳しくは以下を参照するとよいでしょう。
https://docs.astro.build/en/reference/directives-reference/

これで修正が完了しました。最終的には以下のようになります。

---
import Layout from "../../layouts/Layout.astro";
import { getAllPostIds, getPostData } from "../../lib/post";
import type { PostData } from "../../types/postData";
import dayjs from "dayjs";


export const getStaticPaths = async () => {
  const paths = await getAllPostIds();
  return paths
};


const { id } = Astro.params;
const postData: PostData = await getPostData(id!);


const _blogContent = postData.blogContentHTML;
let blogContent = "<div>Not founded Blog Content</div>";
if (_blogContent !== undefined) {
  blogContent = _blogContent;
}
---


<Layout pageTitle={postData.title}>
  <div class="md:mt-10 px-4 md:px-24">
    <article class="flex flex-col m-auto max-w-screen-md justify-center">
      <div class="flex flex-col space-y-1 md:space-y-2">
        <h1 class="text-xl md:text-4xl mt-10 text-left">
          {postData.title}
        </h1>
        <div class="flex text-xs text-giray/50 space-x-2 justify-left">
          <div>
            投稿日 {dayjs(postData.publishedAt).format("YYYY-MM-DD")}
          </div>
          <div>
            最終更新日 {dayjs(postData.updatedAt).format("YYYY-MM-DD")}
          </div>
        </div>
      </div>
      <div
        set:html={blogContent}
        class="prose md:prose-xl text-xs md:text-xl py-10 w-full markdown"
      />
      <div class="md:my-10">
        <a
          href={`/page/${postData.pageId}`}
          class="text-xs md:text-xl hover:cursor hover:text-blue-300"
        >
          トップページへ戻る
        </a>
      </div>
    </article>
  </div>
</Layout>


この手順で他のファイルも修正してきましょう。

その他の修正箇所

今回は、修正箇所の多いファイルを例にして手順をまとめてみましたが、他に重要な点を以下にまとめていきます。

childrenは<slot /> に変更する。

Next.jsではchildren を関数コンポーネントの引数に指定することで、親コンポーネントの間に要素を挟み込むことができていました。

// SampleLayout.tsx
const SampleLayout = ({ children }: { children: ReactNode }) => {
    return <div>{children}<div>;
};

// <SampleLayout>sample</SampleLayout> といった記述が可能になる


Astroでは<slot /> がこれの代わりになります。

// SampleLayout.astro
<div>
    <slot />
</div>

// <SampleLayout>sample</SampleLayout> といった記述が可能になる


ヘッド部は個別にコンポーネントを作成する

Next.js(React)では_document.tsx というファイル名でヘッド部を構築していましたが、astroでは自作する必要があります。

src/layouts 直下に、Document.astro を作成し構築していきましょう。
<head> タグの書き方は通常のHTMLと同じです。
body タグに<slot /> と記載することで、Documentコンポーネントで囲うことができます。

<html>
  <head>
    <meta charset="utf-8" />
    <title>ページタイトル</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  </head>
  <body>
    <slot />
  </body>
</html>


これをレイアウトの外で囲うことで、headの内容が反映されます。

<Document>
    <div> 
    {ページのレイアウトを記述する}
    </div>
</Document>


その他の変更に関しては繰り返しになりますが、Migration Guide(https://docs.astro.build/en/guides/migrate-to-astro/from-nextjs/)を参照すれば大抵のことは書いてあると思います。

今回は以上になります。手順が多いように見えますが、かなり形式的に実施できるので慣れてくると早いかと思います。
迷っている方はぜひ移行してみてください。