BloggerからAstroで再構築してCloudflare Pagesへ移行しました
はじめに
ある日、Bloggerから次のような内容のメールが届きました。
Blogger では、サービスの定期メンテナンスを実施しており、それに伴い、サービス上のごく一部の動画を削除する必要があります。このメンテナンスに伴い、お客様が所有する 3 本の動画が削除対象であることを確認しました。これらの動画は通常、ブログ上では再生できない静止画のスクリーンショットとして表示されています。
必要な対応: Google データ エクスポートにアクセスし、Blogger データのアーカイブをダウンロードして、データのコピーを保存することをおすすめします。これらの動画が Blogger から完全に削除される前に、30 日以内にダウンロードして保存する必要があります。
このブログ(diary.syarihu.net)は2012年から運用していますが、ここ数年はほぼ更新しておらず、正直なところ放置気味でした。しかし、過去に書いた記事の動画や画像がプラットフォーム側の都合で消えてしまうのはやはり避けたいところです。
とはいえ、Bloggerの管理画面から動画を貼り直したり、自前で一からサイトを作り直したりするのも面倒だな…と感じていました。そこで、「AIエージェントに依頼すればサクッと簡単に移行できるのでは?」と思い立ち、Antigravity CLIを使ってGemini 3.8 Flashに移行作業をほぼ丸投げしてみることにしました。
今回は、私が伝えた要件やCloudflare Pagesを選んだ理由、そしてGeminiがどのようにブログを再構築してくれたのかを紹介します。
Gemini 3.8 Flashへの依頼と最初のやり取り
実際にGemini 3.8 Flashへ投げた最初のプロンプトはシンプルで、ブログのURLを添えてざっくりと次の3点の要件を伝えただけです。
- ホスティング先をCloudflare Pagesにすること
- 記事をMarkdownで書いてHTMLで出力できること(従来のURL構造も維持)
- デザインはすべておまかせ
指示を受け取ったGeminiは、すぐに手元の環境(Node.jsなどのツールチェーン)や、用意してあったBloggerのエクスポートデータ(/tmp/blogger_feed.json)を自律的にシェルコマンドで調査し始めました。記事数や過去記事のデータ構造を把握した上で、次のような選択肢を提案してくれました。
- 作業の進め方: 「Astroで環境構築+既存記事57件のMarkdown移行+ブログデザイン実装まで一気に進める」
- 画像の保存方針: 「記事内の画像もローカル(public/images)にダウンロードして永続化する」
フレームワークにAstroを使うことや、画像のリンク切れを防ぐためにローカル永続化する方針は、すべてGemini側が自発的に提案してくれたものです。私は推奨された選択肢を承認しただけで、具体的な実装へと一気に進んでいきました。
なぜCloudflare Pagesを選んだのか
ホスティング先にCloudflare Pagesを指定したのには、いくつか明確な理由があります。
他の個人サイトもCloudflare Pagesへ移行していた
直近で、自分の他の個人サイト(syarihu.dev)をFirebase HostingからCloudflare Pagesへ移行したばかりでした。
移行した理由は、DroidKaigi 2026での登壇でした。私の発表が同時通訳セッションとして選ばれたのですが、スライドに日本語と英語を両方併記するのは、スライド1枚あたりの情報量的に少し難しいと感じていました。
そこで、スライド自体に無理に英語を詰め込む代わりに、スライド画像とともに英語解説を読めるサイト(https://syarihu.dev/slides/droidkaigi2026/)を別途作りました。しかし、大量のスライド画像を配信する場合、これまで使っていたFirebase Hostingだとすぐに転送量の上限へ達してしまうため、これを機にCloudflare Pagesへ移行したという経緯がありました。
そのときの移行体験がとてもよく、設定のシンプルさや配信の快適さを実感していたため、今回のブログも同じ環境に揃えたいと考えました。
転送量の上限がなく、無料で安心して運用できる
これまで使っていたFirebase Hostingの場合、無料枠(Sparkプラン)ではデータ転送量に月間10 GB(1日あたり360 MB)という上限が設定されています。
テキスト中心のページであれば十分ですが、画像が大量に含まれるサイトだと、急なアクセス増で転送量上限に達してサイトが止まったり、課金が発生したりするリスクが付きまといます。
一方、Cloudflare Pagesの無料プランは帯域幅(データ転送量)が無制限です。今回のブログも過去10年分以上の記事と400枚を超える画像が含まれており、画像を手元に保存して自前で配信する構成にしても、転送量を気にせず完全無料で運用できるのは静的ブログにとって非常に大きな安心材料でした。
Git連携とカスタムドメインの手軽さ
GitHubのリポジトリにプッシュするだけで自動的にプレビュー環境や本番環境へビルド・デプロイされる仕組みが最初から整っています。また、独自ドメイン(diary.syarihu.net)の設定も簡単で、インフラの管理コストをほぼゼロにできます。
Geminiによる技術選定とデザイン(Astroの採用)
「デザインもフレームワークも任せる」と伝えたところ、Geminiが選んだのは静的サイトジェネレータのAstro(バージョン7)でした。
選定の理由は次のとおりです。
- Zero-JSベースの高速配信: ブログのようなコンテンツ主体のサイトに対して、デフォルトで余計なJavaScriptを出力しないため、非常に軽量で高速なページを生成できる。
- Content Layer API: Astroの
astro:contentを使うことで、Markdown記事のフロントマターをZodスキーマで型安全に管理できる。 - 柔軟なHTML出力:
build.format: 'file'オプションを使うことで、Blogger時代の.html付きURLをそのまま出力できる。
デザイン面も完全に任せたところ、Tailwind CSS v4をベースにしたすっきりとしたレイアウトを組んでくれました。さらに、ブログとして快適に読めるモダンな機能も自発的に実装してくれました。
- システム設定連動+手動切り替えができるダークモード
- 記事見出しから自動抽出される目次(Table of Contents)
- コードブロックのシンタックスハイライト(Shiki)とワンクリックコピーボタン
- 年・月別のアーカイブ一覧およびタグ一覧ページ
- RSSフィード(
/rss.xml)とサイトマップ(/sitemap-index.xml)の自動生成
移行作業の詳細
Geminiが実際に行った移行作業の流れは、次のとおりです。
1. データの変換と画像のローカル永続化(scripts/migrate.js)
BloggerのAtomフィードをJSON形式(alt=json)で取得したデータから全記事のHTMLを抽出し、Markdownへ変換するNode.jsスクリプトを作成してくれました。
変換にはTurndownとJSDOMを使用し、次のようにコードブロックの言語をクラス名から自動判定したり、埋め込みコンテンツを保持したりするルールを組み込んでいます(実際のコードから抜粋しています)。
import TurndownService from 'turndown';
function createTurndownService() {
const turndown = new TurndownService({
headingStyle: 'atx',
hr: '---',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
emDelimiter: '*',
});
// ツイートやスライドの埋め込みはHTMLのまま残す
turndown.keep(['iframe', 'script']);
// PREタグのクラス名から言語を判定してコードブロックに変換
turndown.addRule('fencedCodeBlocks', {
filter: (node) => node.nodeName === 'PRE',
replacement: (content, node) => {
const codeEl = node.querySelector('code');
const text = codeEl ? codeEl.textContent : node.textContent;
const classNames = (node.className + ' ' + (codeEl ? codeEl.className : '')).toLowerCase();
let lang = '';
if (classNames.includes('language-kotlin') || classNames.includes('.kt')) lang = 'kotlin';
else if (classNames.includes('language-java') || classNames.includes('.java')) lang = 'java';
else if (classNames.includes('language-xml') || classNames.includes('.xml')) lang = 'xml';
else if (classNames.includes('language-bash') || classNames.includes('bash')) lang = 'bash';
else if (classNames.includes('language-js') || classNames.includes('javascript')) lang = 'javascript';
return `\n\n\`\`\`${lang}\n${text.trimEnd()}\n\`\`\`\n\n`;
},
});
return turndown;
}
また、Googleのサーバー(bp.blogspot.comなど)に置かれていた画像は、親の<a>タグが参照している元画像URLをたどって8並列でダウンロードし、public/images/posts/YYYY/MM/slug/へ保存してくれました。記事内に埋め込まれていた画像全469枚がすべて手元にダウンロードされ、Markdown内の画像リンクもローカルパスに差し替えられたため、少なくとも画像に関しては、将来Bloggerがサービス終了したり画像を削除したりしてもリンク切れになる心配はなくなりました。
さらに、Twitter(X)のツイート埋め込みやSpeaker Deckのスライド埋め込みも、そのまま表示できるようにHTMLタグを保持してMarkdownへ変換してくれています。
ここで「少なくとも画像に関しては」とわざわざ書いたのには理由があります。実はこの時点で、動画のほうには落とし穴が残っていました。
2. 動画の救出(そもそも動画ではなかった)
ここで、そもそもの移行のきっかけだった「3本の動画」の話に戻ります。
移行が終わったあとに記事を見返していて気付いたのですが、動画はどこにも復元されていませんでした。おかしいと思って調べてみると、移行前のBloggerの記事HTMLには、動画の埋め込みを示す<iframe class="BLOGGER-VIDEO">やvideo.g?token=といった痕跡が、全57記事を通して1つも存在しませんでした。
代わりに見つかったのが、次のHTMLです。
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/.../s1600/MOV_XXXX.mp4">
<img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/.../s400/MOV_XXXX.mp4" width="400" />
</a>
動画が、動画としてではなく画像アップロードの経路で投稿されていたのです。拡張子は.mp4なのに<img>タグで参照されており、Googleの画像配信側がmp4から静止画を1コマ生成して返していました。冒頭のメールにあった「これらの動画は通常、ブログ上では再生できない静止画のスクリーンショットとして表示されています」とは、まさにこの状態のことだったわけです。
当然、移行スクリプトもこれをただの画像として扱い、静止画1枚をダウンロードして終わっていました。つまり移行した時点では、動画の中身は失われたままだったことになります。
そこで、メールで案内されていたとおりGoogle データエクスポートからBloggerのアーカイブを取得したところ、Albums/ 配下にmp4の実体が3本そのまま入っていました。どれも1080p・30fps・音声付きで、劣化のないオリジナルです。長さは20秒程度のものから2分半を超えるものまでありました。
3本のうち、記事中で実際に使われていたのは1本だけで、残りの2本はどの記事からも参照されていませんでした。使われていた1本をpublic/videos/に配置して<video>タグで置き換えます。ポスター画像には、Bloggerが生成していたあの静止画をそのまま流用しました。
<video controls preload="metadata" playsinline poster="/images/posts/YYYY/MM/slug/img-06.jpg">
<source src="/videos/posts/YYYY/MM/slug/movie.mp4" type="video/mp4" />
</video>
これで、何年も前に撮った映像が、静止画ではなくちゃんと再生できる状態でブログに戻りました。
なお、残り2本をリポジトリに入れなかったのには理由があります。Cloudflare Pagesには1ファイル25 MiBという上限があり、この2本はどちらもそれを大きく超えていたため、そのままでは配信できません。どうしても載せるなら再エンコードが必要になります。帯域は無制限でも、ファイルサイズは無制限ではないという点は覚えておくとよさそうです。
3. URL互換性の完全維持
Bloggerのパーマリンクはhttps://diary.syarihu.net/2020/03/qiitablogger.htmlのように末尾が.htmlで終わります。
Astroではastro.config.mjsにbuild.format: 'file'を指定することで、ディレクトリ形式(/slug/index.html)ではなくファイル形式(/slug.html)で静的ファイルを出力できます。実際の設定は次のとおりです。
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://diary.syarihu.net',
build: {
format: 'file',
},
vite: {
plugins: [tailwindcss()],
},
integrations: [sitemap()],
});
ルーティングをsrc/pages/[year]/[month]/[slug].astroとして設定し、フロントマターのslugにYYYY/MM/slug-nameを指定することで、従来のURLと1文字も違わない完全一致のパーマリンクが生成されます。過去の被リンクや検索エンジンの評価を落とさずに移行できました。
さらに、Bloggerのタグ検索URLやフィードURLへのアクセスも、Cloudflare Pagesのpublic/_redirectsで301転送を設定して救済しています。
/search/label/:tag /tags/:tag 301
/feeds/posts/default /rss.xml 301
このようにリダイレクトルールを記述しておくことで、旧ブログのURL構造でアクセスされた場合でも、新しい対応ページへ自動転送されます。
4. 初期コミットと運用・デプロイ手順の整備
Geminiは必要なファイルを生成して終わりではなく、次のようなことまで自発的にやってくれました。
- Gitリポジトリへの初期コミット: 構築したサイトと移行済み記事をまとめて初期コミットまで済ませてくれた。
- 新規記事作成スクリプトの作成: 今後記事を書くときのために、
npm run new "記事タイトル"を実行するとフロントマター付きのMarkdownファイル(src/content/posts/YYYY-MM-slug.md)が自動生成されるスクリプトを用意してくれた。 - 丁寧なREADME.mdの作成: ローカル開発サーバーの起動方法から、約1秒で全HTMLを出力できるビルド確認コマンド、そしてCloudflare Pages側のビルド設定(Framework presetや
NODE_VERSION=22の環境変数設定、カスタムドメインの手順)まで網羅したドキュメントをリポジトリ直下にまとめてくれた。
「作業が完了したので、ゆっくり確認してみてほしい」という報告とともに、次に人間が何をすればいいのか(ローカル確認、デプロイ設定など)がすべて整理された状態で手元に戻ってきました。指示を出した人間側は、先ほどの動画の件を別にすれば、ほとんど「提案を眺めて承認ボタンを押しただけ」で移行が終わってしまったことになります。
まとめ
Bloggerからの「動画を削除する」という通知メールをきっかけに始めたブログ移行でしたが、Gemini 3.8 Flashに要件を伝えて丸投げしたことで、あっという間にモダンな静的ブログへ生まれ変わりました。
今回の移行で達成できたポイントは次のとおりです。
| 項目 | Blogger時代 | 移行後(Astro + Cloudflare Pages) |
|---|---|---|
| 執筆環境 | Web上のWYSIWYGエディタ | Git + Markdown(ローカルエディタで快適執筆) |
| メディア管理 | Googleの画像サーバーに依存 | リポジトリ内に全画像+動画をローカル保存 |
| 動画 | 再生できない静止画として表示 | mp4を復元して<video>で再生可能 |
| ホスティングコスト | 無料 | 無料(Cloudflare Pagesの無制限帯域) |
| URLの維持 | Blogger固有のURL | build.format: 'file'で完全互換 |
| サイト速度 | プラットフォームのスクリプトで重め | 必要最小限のJSだけの静的HTMLで爆速 |
一方で、AIに丸投げしたままでは拾いきれない部分もありました。動画がそもそも画像として投稿されていたことに気付けたのは、移行後に自分で記事を見返して「あれ、動画はどこへ行った?」と確認したからです。エージェントは指示された範囲をきれいにこなしてくれますが、何が失われているかは、元のデータを知っている人間にしか分からないという当たり前のことを再認識しました。
昔作ったブログを長年放置していて、プラットフォームの仕様変更やデータ削除のアナウンスに困っている方は多いと思います。「移行作業が面倒そう」と後回しにしがちですが、いまならAIコーディングエージェントに要件を伝えるだけで、移行スクリプトからモダンなUI構築まで一気に進めてくれます。
同じように個人ブログの移行を考えている方は、ぜひAIに相談しながら試してみてください。