yamakenji blog

個人ブログを開設しました

2021/12/09

2022/03/01

こんにちは、@yamakenjiです。
この記事は、SLP KBIT Advent Calendar 2021の9日目の記事です。

はじめに

今回はRemixを用いて、自身のブログを構築していきます。
元々Next.jsで途中まで開発していましたが、RemixがOSSとして公開されたので使ってみることにしました。
本記事では、主に実装した機能とVercelへのデプロイ周辺に関して説明していきます。
詳細なコードに関しては、GitHubからご覧ください。blog.yamakenji

(追記) Remix v1.2.3にアプデ、mdからmdxに変更

2022年3月1日時点の、Remix v1.2.3にアプデ、mdからmdxに変更しました。 本記事は元記事に対して、遭遇していたエラーとそれに対する対処を追記しています。

技術構成

  • Remix v1.2.3
  • TypeScript
  • Tailwind
  • Vercel

Remixとは

Remixは、11/23日にv1が正式にリリースされました。
Remixは主に4つの視点に着目しています。

  1. サーバ/クライアントモデルの採用
  2. 従来のWebの基礎となるブラウザ、HTTP、HTMLと連携する
  3. JSを使用しブラウザの動作をエミュレートし、UXを向上させる
  4. 基盤となる技術を過度に抽象化しない

RemixはNext.jsと違いSSGの機能を持たない代わりに、SSRをエッジサーバーで実行します。
SSRでFetchしたデータは、クライアントに空のレスポンスを返す前にサーバ上で全てのデータを取得して、キャッシュします。 キャッシュ内容は、routeコンポーネント毎にcatche-headerを詳細に設定することができます。
これにより、従来用意していたスケルトンUIなどが必要なくなりました。
さらには、<Link prefetch> で設定しているものは、ボタンをホバーした時などにlink先のデータをprefetchし、サーバー上に期間内でキャッシュします。実際にクリックした際には、キャッシュされたデータを利用します。これは、ブラウザの機能の一つであるリンクの先読みをラップして使用しています。  
その他にも機能がたくさんあるので、興味ある人は公式まで。 Remix

実装

ということで実装していきます。
今回主に実装したのは、mdxの記述、タグ、カテゴリー一覧の抽出、OGP画像の生成です。

MDXへの対応

元記事では、記事をmarkdownで管理し、remarkでhtmlに変換していました。 しかし、Vercelがremixでのfilesystemを正式に対応しておらず、 記事をdbで管理するか、GitHub APIを通して取得するかの方法しかありませんでした。 (正確には、1.0.6以前のバージョンでvercelの設定ファイルを用いて、serverless functions内で参照できるように記述してあげれば使用できましたが、 Remixのversionをあげた際に対応がめんどくさいので諦めました)

Remixでは、標準でMDXに対応しており、route下に配置することで表示が可能です。 また、記事一覧として、各記事情報を取得するには、全てのMDXをimportする必要があります。 正直、記事が増えるたびにimportする必要があり、拡張性がない方法です。 公式でもそのように言及されていますが、「記事を書くのは大変なことであり、管理が大変になったという素晴らしい問題に直面してから考えよう」とも記述されています。 そのため、拡張性については今後の課題とします。

import * as firstPost from '../routes/blog/first-post.mdx';

const blogs = [firstPost];

MDXから属性情報の取得

全ての記事を取得して、いい感じに管理できるようにします。

function postFromModule(mod: any): Blog {
  const slug: string = mod.filename.replace(/\.mdx$/, '');
  const updatedAt: string = !mod.attributes.updatedAt ? mod.attributes.createdAt : mod.attributes.updatedAt;
  const tags: string[] = !mod.attributes.tags ? [] : Array.from(new Set(mod.attributes.tags));

  return {
    ...mod.attributes,
    slug,
    updatedAt,
    tags,
  };
}

export function getBlogBySlug(slug: string): Blog {
  const blogs = getAllBlogs();
  const blog = blogs.find((blog) => blog.slug === slug);

  if (!blog) {
    throw new Error(`Blog not found: ${slug}`);
  }

  return blog;
}

export function getAllBlogs(): Blog[] {
  return blogs.map(postFromModule);
}

カテゴリー、タグ一覧の取得

取得した記事一覧から、カテゴリー一覧とタグ一覧を取得していきます。
これらの情報は一意に限定されるものであり、重複したものは除外していきます。

export function getAllTags(): string[] {
  const blogs = getAllBlogs();
  const tags = blogs.map((blog) => blog.tags).flat();
  return Array.from(new Set(tags));
}

export function getAllCategories(): string[] {
  const blogs = getAllBlogs();
  const categories = blogs.map((blog) => blog.category);
  return Array.from(new Set(categories));
}

loaderはRemixが用意しているReactのライフサイクルみたいなモジュールAPIで、レンダリングされる前にサーバー側で呼び出します。また、引数を受け取ることができ、例えば/$slugみたいなURLからそのslugの中身を動的に抽出することもできます。
一覧データは、全てのコンポーネントで利用することを想定しているため、ルートコンポーネントでloaderから呼び出して、不必要なリクエストを削減します。

export const loader: LoaderFunction = async () => {
  const [tags, categories] = await Promise.all([getAllTags(), getAllCategories()]);
  const data: LoaderData = { tags, categories };

  return json(data, {
    headers: {
      'Cache-Control': `public, max-age=${60 * 10} s-maxage=${60 * 60}`,
    },
  });
};

タグ、カテゴリーから記事を取得

link等で/tag/$slugや/category/$slugにアクセスしたときに、そのタグやカテゴリーに関連する記事を取得していきます。 loaderで、Route Paramsを取得して、その情報をもとに取得します。
例えば、カテゴリーの場合は以下のようになります。

export const loader: LoaderFunction = async (content: any) => {
  const category = content.params.slug;
  const blogs = await getBlogsByCategory(category);
  const data: LoaderData = { blogs, category };

  return json(data, {
    headers: {
      'Cache-Control': 'public, max-age=60 s-maxage=60',
    },
  });
};
export function getBlogsByCategory(category: string): Blog[] {
  return getAllBlogs().filter((blog) => blog.category === category);
}

その他

その他にも、OGPなど、htmlのmeta要素を動的に設定しています。
Remixでは、meta functionをexportすることで、各Routeごとにmeta要素を設定することができます。
また、OGP画像の生成として、vercelが公開しているog-imageを使用しています。誰でもフォークして、中のテンプレートを編集してデプロイすることで、OGP画像を自分で生成できます。

Vercelにデプロイ

デプロイ先として、Vercelを使用しています。他にも候補として、Cloudflare Workersがありましたが、他のプロジェクトでVercelを使用していたため、今回はVercelにしました。
Remixが公開された直後では、Vercelにデプロイする際にはいくつかの設定が必要でしたが、現在は設定なしでデプロイすることが可能です。 また、devDependenciesにvercelをインストールしていた場合は、23.1.3以上に上げておく必要があります。(インストールしていない場合は、問題なさそうです) これは、Remixがzero-configで使用しているバージョンが23.1.3からであり、vercelにデプロイした場合、404エラーの原因になります。

まとめ

Remixを用いて、ブログを構築していきました。
自身のブログを開設したので、これからどんどん更新していきたいと思います。
また、機能をどんどん追加していきたいと考えていますので、気軽にissueやPRを投げてもらえると嬉しいです。
特に、今回は実装していないページネーションなども実装していく予定です。