2026-01-17

個人ブログを Hugo on CloudFront + S3 から Astro on Vercel に移行した

2025 年の年始に、個人ブログのサイト生成ツールを Hugo から Astro に、ホスティング環境を AWS (CloudFront + S3) から Vercel に移行しました。移行の理由や実際の移行手順、移行時に直面した課題などについてまとめます。

概要

このブログは元々 Hugo を使って構築し、 AWS の CloudFront + S3 環境でホスティングしていました。今回、サイト生成ツールを Astro に、ホスティング環境を Vercel に変更しました。

移行前の環境

移行前の環境は以下の通りでした。

Hugo は Go 製の静的サイトジェネレーターで、ビルドが非常に高速なことが特徴です。長年このブログを Hugo で運用してきましたが、いくつかの課題を抱えていました。

移行を決めた理由

移行を決めた主な理由は以下の 4 点です。

1. インフラ構成がコード管理されていない

長年運用してきた結果、ビルド設定やインフラ構成がコード管理されておらず、各種アップデートが煩雑になっていました。当初は AWS CLI やマネジメントコンソールから手動で設定していたため、設定の履歴が追いづらく、変更内容を把握するのも困難になっていました。

2. テーマの編集がしづらい

Hugo のテーマは Git サブモジュールとして管理していましたが、カスタマイズする際にテーマファイルを直接編集するか、オーバーライド用のファイルを別途用意する必要がありました。これにより、どのファイルがカスタマイズされているのか把握しづらく、保守性が低下していました。

3. Hugo 独自のショートコードが秘伝のタレ化

Hugo には独自のショートコード機能があり、便利に使っていましたが、時間が経つにつれて独自ショートコードが増え、管理が煩雑になっていました。特に下記のようなショートコードを多用していました。

{{</* inner-link "/post/some-article" */>}}
{{</* lazy "image.png" "説明" */>}}

これらのショートコードは Hugo 環境に依存しており、他の環境への移行が困難になっていました。

4. 新年を迎えての心機一転

2025 年の新年を迎え、ブログ環境を一新したいという気持ちが強くなりました。これが最も大きな理由かもしれません。

Astro を選んだ理由

新しいサイト生成ツールとして Astro を選んだ理由は以下の通りです。

TypeScript でサイトを構築できる

Astro は TypeScript を標準でサポートしており、型安全な開発が可能です。静的サイトジェネレーターでありながら、必要に応じてインタラクティブな要素も追加できる柔軟性も魅力的でした。

ブログ用のテンプレートが豊富

Astro には公式のブログテンプレートやコミュニティによる多様なテンプレートが用意されており、移行が比較的簡単だと考えました。実際、既存の Hugo コンテンツを Astro の Content Collections として移行するのは、フロントマターの形式がほぼ共通していたため、スムーズに進みました。

シンプルな構成

Astro のプロジェクト構成はシンプルで理解しやすく、必要最小限の設定で始められる点も魅力的でした。

実際の移行手順

実際の移行手順は、このリポジトリの Git ログを参照すると分かりやすいです。主要なコミットは fa7a1ac 以降になります。

1. Astro プロジェクトのセットアップ

まず、Astro プロジェクトをセットアップしました。

npm create astro@latest

基本的な設定は以下の通りです。

// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://michimani.net',
  integrations: [sitemap()],
  srcDir: 'src',
  trailingSlash: 'never',
  markdown: {
    syntaxHighlight: {
      type: 'shiki',
      excludeLangs: ['mermaid', 'math'],
    },
  },
});

シンタックスハイライトには Shiki を使用し、Mermaid や数式は除外するよう設定しています。

追加したプラグイン

デフォルトの構成に加えて、以下のプラグインを追加しました。

RSS フィードの生成

@astrojs/rss を使って RSS フィードを生成するようにしました。これは GitHub のプロフィールページにブログの最新記事を表示するために必要でした。

サイトマップの生成

@astrojs/sitemap を使ってサイトマップを自動生成するようにしました。生成されたサイトマップは Google Search Console への登録に利用しています。

Mermaid 記法のサポート

Mermaid 記法を使って図を描画できるようにしたかったため、当初は rehype-mermaid を使ってサーバーサイドでレンダリングしようと考えました。しかし、Vercel 環境では Playwright のインストールがうまく行かなかったため、クライアントサイドでレンダリングする方式に切り替えました。具体的には、mermaid.esm.min.mjs を読み込んでクライアント側で Mermaid ブロックをレンダリングしています。

2. コンテンツの移行

Hugo の content/post/ 配下にあった Markdown ファイルを Astro の src/content/blog/ に移行しました。

# Hugo のコンテンツディレクトリ構造
content/
└── post/
    ├── aws/
    ├── development/
    ├── programming/
    └── ...

# Astro のコンテンツディレクトリ構造
src/
└── content/
    └── blog/
        ├── aws/
        ├── development/
        ├── programming/
        └── ...

フロントマターの形式はほぼ共通していたため、大きな変更は不要でした。

---
title: "記事のタイトル"
date: 2026-01-17T00:00:00+09:00
tags: ["タグ1", "タグ2"]
categories: ["カテゴリ"]
url: "/post/article-slug"
draft: false
---

3. Hugo ショートコードの置き換え

Hugo 独自のショートコードを標準の Markdown 記法に置き換えました。

Hugo で使っていた inner-link ショートコードを通常の <a> タグに置き換えました。

<!-- 置き換え前 -->
{{</* inner-link "/post/some-article" */>}}

<!-- 置き換え後 -->
[記事タイトル](/post/some-article)

lazy ショートコードの置き換え

画像の遅延読み込み用の lazy ショートコードを通常の <img> タグに置き換えました。

<!-- 置き換え前 -->
{{</* lazy "image.png" "画像の説明" */>}}

<!-- 置き換え後 -->
![画像の説明](https://img.michimani.net/blog/image.png)

これらの置き換えは、Git のコミット履歴を見ると複数回に分けて実施していることが分かります。

4. 画像配信環境の構築

画像ファイルの配信環境については、以下のような構成にしました。

Hugo 時代の画像管理

Hugo 時代から、画像ファイルは Git 管理せず、Hugo で生成したファイルと同じ S3 バケットにアップロードしていました。

新しい画像配信環境

Astro への移行に伴い、画像配信環境を以下のように変更しました。

これにより、Astro 移行後の記事では画像のパスはそのままに、ドメインを img.michimani.net に変更して画像を読み込むようになりました。

インフラのコード管理

移行前の課題であった「インフラ構成がコード管理されていない」という問題を解消するため、今回新たに作成した CloudFront ディストリビューション、ACM 証明書、Route 53 のレコードなどのリソースは Terraform で管理するようにしました。

# 例: CloudFront と ACM の定義
resource "aws_cloudfront_distribution" "image_cdn" {
  # CloudFront の設定
}

resource "aws_acm_certificate" "image_domain" {
  # SSL 証明書の設定
}

resource "aws_route53_record" "image_domain" {
  # DNS レコードの設定
}

これにより、インフラの変更履歴が Git で管理でき、変更内容の把握や他環境への展開が容易になりました。

<!-- Hugo 時代 -->
{{</* lazy "blog/image.png" "画像の説明" */>}}

<!-- Astro 移行後 -->
![画像の説明](https://img.michimani.net/blog/image.png)

この方式により、画像ファイル自体は移動せず、既存の S3 バケットをそのまま活用しながら、新しい配信環境に移行できました。

5. Vercel へのデプロイ

最後に、Vercel にデプロイしました。Vercel は GitHub リポジトリと連携するだけで、自動的にビルドとデプロイを行ってくれます。

graph LR
    A[GitHub Push] --> B[Vercel Trigger]
    B --> C[Build]
    C --> D[Deploy]
    D --> E[公開]

設定はほぼデフォルトのままで、ビルドコマンドとして pnpm build を指定するだけでした。

移行後の構成

移行後の環境は以下の通りです。

移行時の課題

移行時にいくつかの課題に直面しました。

URL の正規化

移行直後、記事の URL 末尾にスラッシュが付いてしまう問題が発生しました。これは Astro の trailingSlash 設定を never にすることで解決しました。

export default defineConfig({
  trailingSlash: 'never',
});

シンタックスハイライトの言語指定

Hugo と Astro でシンタックスハイライトのライブラリが異なるため、コードブロックの言語指定を一部修正する必要がありました。特に、Hugo で使っていた独自の言語指定が Shiki でサポートされていないケースがありました。

Renovate の設定

依存関係の自動アップデートツールとして Renovate を導入しました。これにより、Astro や関連パッケージのアップデートを自動的に検出し、プルリクエストを作成してくれるようになりました。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"]
}

まとめ

個人ブログを Hugo on CloudFront + S3 から Astro on Vercel に移行したことについて書きました。

移行後は、Vercel の自動デプロイにより記事の公開が非常に簡単になりました。また、プルリクエスト単位でプレビュー環境が自動的に作成されるようになったため、Renovate によるライブラリアップデートの動作確認もやりやすくなりました。依存関係のアップデートが自動的にプルリクエストとして作成され、プレビュー環境で動作を確認してからマージできるため、安心してライブラリを最新に保てるようになりました。

今年はアウトプットを以前のように増やしていきたいと思っているので、無理しない程度に頑張っていきます。