作成日: 2025-12-01 15:08:00

更新日: 2025-12-13 06:39:55

368 views

自作サイトで頑張る Advent Calendar 2025
技術
Web

このページについて

この記事は「自作サイトで頑張る Advent Calendar 2025」の 1 日目です。

はじめに

本記事では、この個人ページを構築した際の技術的な話を紹介していきます。 将来このページをいじる時に迷わないように、備忘録の意味も込めています。

フロントエンド

フレームワーク

フロントエンドには React Router v7 を使用しています。 当初は「Remix っていうフロントエンド F/W が流行ってるらしい」ということで試していましたが、気づいたら React Router と合流してました。

React Router v7 になってからは v6 と同様にファイルベースルーティングをする必要はないようですが、私は当初の思想を継いで Flat Routes を採用しています。

Next.js と比較して、loader や action などの仕組みがシンプルで分かりやすいのが良かったです。また、Vite や Express などどこにでも乗るので F/W にロックインされないのも魅力的でした。 また、インフラ面でも Next.js よりシンプルに構築できました。この辺の話は明日の記事で詳しく紹介します。

逆に言えば、それくらいしか特徴は感じられなかったです。フロントは専門じゃないので許して。

UI ライブラリ

UI ライブラリには Tailwind CSSDaisyUI を使用しています。 JavaScript から解放されたいというのが主な選定理由です。

多くのケースで、UI の構築に JavaScript を使用すると、パフォーマンスが著しく低下します。 例えば、スクロールに追従するような UI を JavaScript で実装すると、スクロールに対して少し遅れて UI が動作します。

このような思想を出発点に、JavaScript の使用は明示的に実施し、実装の上で把握・管理できるということを重視しました。 その点、Daisy UI では UI のほとんどを className の指定によって実装でき、 どうしても JavaScript が必要な場合には外側で制御するという構成のため、非常に相性が良かったです。 また、こういう意味でも React Router v7 の Web 標準に準拠する指向やシンプルさはマッチしていました。

例えば shadcn/ui などはコンポーネントの内部で JavaScript 機能を多用しているため、今回は採用しませんでした。

本サイトでは執筆時点で JavaScript を無効化しても全てのコンテンツが JavaScript 有効状態と同様に閲覧可能です。 (もちろん、React Router の SPA 機能が使えないため、ページ遷移はフルリロードになります。)

CMS バックエンド

本サイトでは記事の管理に自作のヘッドレス CMS を使用しています。 CMS の開発にあたり、大まかに以下のような要件を設定しました。

  1. マークダウン / MDX で記事を執筆できること
  2. 記事の内容は Web サイト自体のソースコードのバージョニングとは別で管理できること
  3. 編集時に本番環境と同じ見た目でプレビューできること

マークダウン

React Router v7 が Vite + Rollup という構成なので、マークダウンの対応には mdx-js を使用しました。 mdx ファイル上でメタデータの記載をしたかったため、gray-matter で frontmatter をパースしています。

記事管理

よくある方法として、Git リポジトリで記事を管理し、更新時にビルドとデプロイを行うというものがあります。 しかし、この方法では記事の更新とソースコードの更新が同じバージョニングで管理されてしまいます。 例えば、記事の更新だけを行いたい場合でも、ソースコードのバージョンが上がってしまいます。

そこで本サイトでは、記事の管理のために、新たに CMS バックエンドを構築し、動的に記事を配信する方式を採用しました。 また、ローカルから記事を編集するために、CMS バックエンドを叩くための CLI ツールを作成しました。

記事プレビュー

プレビューのための UI を用意するという選択肢もありましたが、フロントの更新時にプレビュー UI も更新する必要があり、メンテナンスコストが高くなります。 そこで、開発環境のみで /articles/$slug/edit というルートを用意し、ここでは /app/articles/$slug.{md,mdx} を直接インポートして表示する方式を採用しました。

routes.ts は次の通りです。

import {
  layout,
  type RouteConfig,
  type RouteConfigEntry,
  route,
} from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";

const articleRoutes =
  process.env.NODE_ENV === "development"
    ? Object.keys(
        import.meta.glob("./articles/**/*.{md,mdx}", {
          eager: true,
          query: "?url",
        }),
      ).map(
        (file): RouteConfigEntry =>
          route(
            file
              .replace("./articles", "/articles")
              .replace(/\.(md|mdx)$/, "/edit"),
            file,
          ),
      )
    : [];

export default [
  ...(await flatRoutes({
    ignoredRouteFiles: ["**/articles.tsx", "**/articles.$slug.tsx"],
  })),
  layout("./routes/articles.tsx", [
    ...articleRoutes,
    route("/articles/:slug", "./routes/articles.$slug.tsx"),
  ]),
] satisfies RouteConfig;

インフラ

概要

本サイトは AWS 上に構築されています。構成図は以下の通りです。

ほとんどをサーバーレスで構築しており、運用コストを抑えています。

フロントエンド

フロントエンドは Lambda + S3 + CloudFront 上にホスティングしています。明日の記事の内容と重複するため、詳細は割愛します。

CMS バックエンド

CMS バックエンドは、S3 + DynamoDB + Lambda で構築しました。DynamoDB に記事のメタデータを保存し、S3 に記事のコンテンツを保存しています。 これらの管理は Lambda 関数で行っています。

メタデータ管理

DynamoDB には記事のタイトル、作成日、更新日、pv、タグ、S3 のオブジェクトキーなどのメタデータを保存しています。

コンテンツ管理

S3 には記事のコンテンツを保存しています。ファイル名に記事のハッシュを使用することで、CloudFront のキャッシュを有効活用しています。 またこれにより、(記事のハッシュを覚えていれば) 過去バージョンにロールバックすることも可能です。 (バージョン管理に関しては将来的に過去バージョンのキーを DynamoDB に保存するなどして、簡単にロールバックできるようにすることも検討しています。)

Lambda 関数

Lambda 関数は OAC 経由で Function URL を CloudFront のオリジンとして使用しています。 記事に関する操作はすべてこの Lambda 関数を通じて行います。直接 S3 や DynamoDB にアクセスすることはありません。

AWS リソースの操作には基本的に AWS SDK を使用していますが、Lambda 関数から S3 への取得リクエストはキャッシュを有効利用するため、CloudFront を経由しています。

ここで、OAC 経由の非 GET/HEAD リクエストにはリクエストボディーの sha256 を計算して x-amz-content-sha256 ヘッダーに含める必要があります。今回、HTTP 通信には OpenAPI TS の openapi-fetch を使用していますが、ミドルウェアでリクエストボディを取得できる機能が見当たりませんでした。そこで、Lambda@Edge を使用して、リクエストボディーの sha256 を計算し、x-amz-content-sha256 ヘッダーに追加する処理を実装しました。

https://dev.classmethod.jp/articles/cloudfront-lambda-url-with-post-put-request/

dev.classmethod.jp

また、OAC を経由する段階で、Sigv4 署名のために Authorization ヘッダーが使用されます。そのため、後述の認可の段階で Authorization ヘッダーを使用できません。そこで、認可用のトークンは KCMS-Init-AuthorizationKCMS-Authorization ヘッダーに含めることにしました。

CLI ツール

ローカルから CMS バックエンドを操作するために CLI ツールを作成しました。 CLI ツールの作成は commander.js を使用しています。 CLI ツールでは、記事の作成、更新、一覧取得などの操作が可能です。

  1. mdx ファイルを作成
  2. 開発環境でプレビュー
  3. CLI ツールで CMS バックエンドに記事をアップロード

という運用を想定しています。

認可

CMS バックエンドの認証は 2 段階で行っています。

  1. API キーをヘッダーに含めて /api/verify エンドポイントにアクセスして JWT トークンを取得
  2. 取得した JWT トークンをヘッダーに含めて各エンドポイントにアクセス

この方法により、マスターの API キーが通信経路に乗る頻度を減らしています。

また、/api/verify エンドポイントは CloudFront のパスパターンにより隠蔽されているため、CLI ツールで直接 InvokeFunction を実行しない限りアクセスできません。そして、InvokeFunction 自体も IAM ポリシーで制限されてるため、3 重の防御となっています。

おわりに

今回、このアドカレに合わせて CMS 機能などを実装しましたが、インフラ構築に熱を入れすぎてフロントエンドが疎かになってしまいました。 今後、記事を書きながら記事表示機能周辺を改善していきたいと思います。

明日はインフラ構築の詳細について紹介しますので、ぜひご覧ください。