自分のサイトを Next.js から Astro に移行した

2023/01/05

wattanx

はじめに

年末から年始にかけて Next.js で作成した自分のサイトを Astro に移行しました。

以前は Next.js, Chakra UI を使用して作成していましたが、できるかぎり JS を減らしたサイトを作りたいと思って Astro に移行しました。

あとついでに Blog も作りました。

技術スタック

  • Astro
  • Tailwind
  • Radix UI
  • Partytown
  • mdx

Astro めっちゃ楽

Partytown や sitemap の設定をintegrationsに書くだけなので、めっちゃ楽で快適でした。

元々 tsx で書いていたコンポーネントをそのまま使えるので移行も簡単でした。(ハイドレーションするかどうかコンポーネント単位で選べるのが良い)

OG Image

OG Image の生成には satori を使用しました。 とりあえずビルド時に OG Image が動的に出力できるようにしています。(svg から png に変換するために sharpを使っています。)

GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG

Enlightened library to convert HTML and CSS to SVG - GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG
title

下記のようなスクリプトを用意してビルド時に OG Image を生成するようにしています。

import satori from "satori";
import sharp from "sharp";
import fs from "fs";

export const generateOgp = async (
  options: { title: string; url: string }[],
) => {
  for (const option of options) {
    const png = await generateOgpImage(option.title);
    fs.writeFileSync(`public/images${option.url}.png`, png);
  }
};

const generateOgpImage = async (title: string) => {
  // フォントデータを読み込む
  const regular = fs.readFileSync("public/fonts/NotoSansJP-Regular.otf");
  const medium = fs.readFileSync("public/fonts/NotoSansJP-Medium.otf");
  // JSX から画像を生成する
  const svg = await satori(
    <div
      style={{
        height: "100%",
        width: "100%",
        display: "flex",
        backgroundImage: "linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%)",
        color: "#fff",
        fontSize: 48,
        fontWeight: 600,
        justifyContent: "center",
        alignItems: "center",
        padding: "0 2rem",
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          padding: "1rem 4rem",
          backgroundColor: "#282b2f",
          justifyContent: "space-between",
          borderRadius: "1rem",
          height: "90%",
        }}
      >
        <p style={{ fontSize: 64, fontWeight: 700 }}>{title}</p>

        <div style={{ display: "flex", alignItems: "center" }}>
          <img
            src="https://avatars.githubusercontent.com/wattanx"
            width={80}
            height={80}
            style={{ borderRadius: "100%" }}
          />
          <p style={{ marginLeft: "16px", fontWeight: 400 }}>wattanx</p>
        </div>
      </div>
    </div>,
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Noto Sans JP",
          data: regular,
          style: "normal",
          weight: 400,
        },
        {
          name: "Noto Sans JP",
          data: medium,
          style: "normal",
          weight: 600,
        },
      ],
    },
  );

  // SVG から PNG 形式に変換する
  const png = await sharp(Buffer.from(svg)).png().toBuffer();

  return png;
};

Playground を確認しながら作成するとサクッと作ることができました。

つまずいたところ

Timeline を作成するにあたって、Tab の Component を作成する必要があったのですが、実装するのが手間なので Radix UI を使いました。

@radix-ui/react-tabsを使用した状態でビルドした場合、下記のようなエラーが表示されてビルドできませんでした。

(yarn devは問題ない)

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.

原因のソースコードは下記のような形で@radix-ui/react-tabsを import しているところにありました。

import * as Tabs from "@radix-ui/react-tabs";

export const TabsRoot = Tabs.Root;

export const TabList = Tabs.List;

export const TabTrigger = Tabs.Trigger;

export const TabPanel = Tabs.Content;

それらしい Issue を見たが原因がわからず、Issue 内に書いてあった対処法 でひとまずビルドが通るようになりました。

対処法

import * as Tabs from "@radix-ui/react-tabs";

// これを入れる
const { List, Root, Trigger, Content } =
  (Tabs as unknown as { default: typeof Tabs }).default || Tabs;

export const TabsRoot = Root;

export const TabList = List;

export const TabTrigger = Trigger;

export const TabPanel = Content;

最後に

今まで技術系の記事は Zenn に書いていたのでどう書き分ければいいか迷います。

できれば、継続的に発信していきたい。