Gatsby製のブログサイトをAstroに移行した。
ビルドが遅い。それだけの理由だった。記事30本程度で2〜3分かかっていたのが、Astroにしたら7秒になった。それだけで移行した甲斐があったと思っている。
ただ、思ったより罠が多かった。同じような移行を考えている人のために、踏んだ地雷を全部書いておく。
1. slug がAstro 5の予約フィールドと衝突する
GatsbyのMarkdownフロントマターでは slug フィールドをURLに使うのが一般的だ。
slug: "what-is-endorphin"
AstroのContent Collectionsでも同じように使おうとして schema に slug: z.string().optional() を定義したが、Astro 5では slug が内部で自動生成される予約フィールドと衝突し、entry.data.slug が undefined になるケースが発生した。
対策はシンプルで、ディレクトリ名からスラッグを導出するユーティリティ関数を作った。
export function deriveSlug(entry: any): string {
if (entry.data?.slug) return entry.data.slug;
// 例: "2025/0105-what-is-endorphin" → "what-is-endorphin"
const dir = entry.id.split('/').slice(-2, -1)[0] || '';
return dir.replace(/^\d{4}[-_]?/, '');
}
2. lazyLoadImage プラグインが記事内画像を壊す
tech.sinayaka.com(同じくAstroで動いているテックブログ)から設定を流用したとき、lazyLoadImage というrehypeプラグインを引き継いだ。このプラグインはスクロール連動の遅延ロードを実現するもので、画像の src を /spinner.gif に差し替えて元のパスを data-src に保存する。
問題は、MDX内の相対パス指定の画像( のような書き方)を使っている場合だ。
Astroはビルド時に相対パス画像を /_astro/asahi.HASH.webp のような最適化済みURLに変換するが、このrehypeプラグインが変換前の相対パスを data-src に保存してしまう。結果として本番環境で画像が404になる。
解決策:元サイト(Gatsby)が画像遅延ロードを使っていないなら、素直にプラグインを外す。
// astro.config.js
markdown: {
rehypePlugins: [], // lazyLoadImageを除去
}
3. Netlifyの環境変数はビルド前に設定しないと反映されない
AstroはSSG(静的サイト生成)なので、import.meta.env.PUBLIC_MAILFORM_URL のような環境変数はビルド実行時にHTMLへ焼き込まれる。Netlifyでデプロイした後に環境変数を追加しても、再ビルドするまで反映されない。
さらに、Netlifyの環境変数には「スコープ」設定がある。
- Builds → ビルド時に使える(Astroはこれが必要)
- Runtime → サーバー関数などの実行時のみ
- All scopes → 両方
「All scopes」か「Builds」を選んでいないと、Astroのビルド中に変数が見えず空文字で焼き込まれる。
4. お問い合わせフォームの no-cors と Content-Type の罠
GASへのフォーム送信に fetch を使っているが、こんな書き方をしていた。
// ❌ バグ:Content-Type がヘッダーとして送信されない
const postparam = {
method: "POST",
mode: "no-cors",
"Content-Type": "application/x-www-form-urlencoded", // トップレベルは無視される
body: JSON.stringify(forms)
};
Content-Type は headers オブジェクトの中に書かないと送信されない。さらに mode: 'no-cors' の場合、Content-Type: application/x-www-form-urlencoded と宣言してJSONボディを送るとGASがパースに失敗する。
動いていた別サイトのコードを調べたら、実は Content-Type がトップレベルに書かれたバグを含んだまま動いていた。ヘッダーが送られないことで text/plain 扱いになり、GASが e.postData.contents をそのまま読めていたという皮肉な結果だった。
// ✅ Content-Type なしで送ると text/plain になりGASが読める
await fetch(url, {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({ site: 'example.com', name, email, message }),
});
5. OGP画像のURLにスラッシュが足りない
export const SITE_URL = 'https://example.com'; // 末尾スラッシュなし
export const SITE_IMAGE = 'thumbnail.png'; // 先頭スラッシュなし
<!-- 結果 -->
<meta property="og:image" content="https://example.comthumbnail.png">
ドメインとファイル名がくっついてLINEでURLを貼ってもアイコンが出ない、という症状になる。どちらかにスラッシュを追加するだけで直る。
export const SITE_IMAGE = '/thumbnail.png'; // 先頭スラッシュを追加
6. SVGファビコンはLINEとChromeの一部で使えない
Astroのスターターは <link rel="icon" type="image/svg+xml" href="/favicon.svg"> だけが設定されていることが多い。SVGファビコンはSafariの一部やLINEのリンクプレビューで表示されないため、PNG・ICO形式も追加しておく必要がある。
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
7. CommonMarkの日本語太字問題
MDXで **コルチゾール(ストレスホルモン)** のように書くと、閉じる ** の直前が全角括弧 ) の場合に太字として認識されないことがある。
原因はCommonMarkの仕様で、** が「right-flanking delimiter」として認定されるには直前がUnicode句読点でないか、直後がUnicode句読点または空白である必要がある。全角括弧はUnicode句読点なので条件を満たさない。
remark-cjk-friendly を入れると解決する。
pnpm add remark-cjk-friendly
// astro.config.js
import remarkCjkFriendly from 'remark-cjk-friendly';
markdown: {
remarkPlugins: [remarkCjkFriendly, ...],
}
まとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
entry.data.slug が undefined | Astro 5の予約フィールド衝突 | ディレクトリ名からslugを導出 |
| 記事内画像が404 | lazyLoadImageが相対パスを保存 | プラグインを外す |
| フォームURLが空 | SSGのビルド時焼き込み | Netlifyのスコープを「Builds」に |
| フォームが届かない | Content-Type誤設定 | no-corsではJSON bodyをそのまま送る |
| OGP画像のURLが壊れている | スラッシュ欠落 | SITE_IMAGEの先頭に/を追加 |
| LINEにアイコンが出ない | SVGファビコン非対応 | PNG/ICOも追加 |
| 日本語の太字が効かない | CommonMarkの仕様 | remark-cjk-friendlyを追加 |
Gatsbyの資産はほぼそのまま移行できたが、細かいところでこれだけ詰まった。特に2・3・4は「動いているはずなのになぜ?」という類で時間を食った。同じ轍を踏む人が減れば幸いだ。