Next.jsの静的サイトをAstroに移行する手順
先日ブログを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専用のコンポーネントは全て削除します。今回の例では、
Head
やLink
等があたります。 - 外部パッケージが必要な場合はインストールします。この例では、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>
にします。className
をclass
にします。
基本的には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/)を参照すれば大抵のことは書いてあると思います。
今回は以上になります。手順が多いように見えますが、かなり形式的に実施できるので慣れてくると早いかと思います。
迷っている方はぜひ移行してみてください。