Astro v4で構築してから今まで放置していたので、最近リリースされたv6まで一気にアップグレードしました。ただAstroバージョンを上げて変更箇所を対応するだけだと思っていたら、v6に上がったことでCloudflareアダプターの更新が必要になり、Vite 7でAstro公式のTailwind統合も使えなくなったのでTailwindのViteプラグインに移行し、さらにCloudflare PagesからWorkersへの移行も必要になったりと、思いのほか面倒でした。 アップグレードの手順はドキュメントを見てくださいと言ったらそれまでなので、ここでは私が管理しているプロジェクトで対応が必要だったものをまとめてみます。
環境
- Astro v4.16.18(一部SSRページ)
- Tailwind CSS v3.4.17(@astrojs/tailwind)
- @astrojs/cloudflare v11.2.0
- Cloudflare Pages
Astro v5への更新
大きな変更が無く修正が容易なので、v5への更新はすんなり終わるはずです。
hybridレンダリングモードの廃止
v4まではSSGとSSRを混在させるためにhybridモードを指定する必要がありましたが、v5からは明示的な指定が不要に。各ページの修正は必要なく、従来通りexport const prerender = false;を宣言するだけでいいです。
// astro.config.mjs
export default defineConfig({
site: "https://example.com",
output: 'hybrid',
adapter: cloudflare(),
https://docs.astro.build/ja/guides/upgrade-to/v5/#removed-hybrid-rendering-mode
コンテンツコレクションをglobローダーで読み込むように
コレクション設定ファイルの置き場所がsrc/content/config.tsからsrc/content.config.ts(src直下)に移動し、type: 'content'の代わりにglobローダーでコレクションを読み込む形に。zodのインポート元もastro:contentからastro/zodに変わっています。ファイルベースのローダーになったのでコンテンツを管理するディレクトリ構造の制限が緩和されました。
// src/content/config.ts → src/content.config.ts に移動
import { z, defineCollection } from 'astro:content';
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const posts = defineCollection({
type: 'content',
loader: glob({ pattern: "**/*.mdx", base: "./src/content/posts" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
cover: z.union([image(), z.string()]).optional(),
tags: z.array(z.string()),
updateDate: z.string().optional(),
showSideToc: z.boolean(),
noIndex: z.boolean(),
}),
});
export const collections = { posts };
https://docs.astro.build/ja/guides/upgrade-to/v5/#updating-existing-collections
コレクションエントリのslugフィールド消滅 & idフィールドの形式変更
Content Layer APIへの移行でエントリのslugフィールドが廃止され、新しい形式のidフィールドに置き換わりました。動的ルーティングの構築だけに使用しているなら単にidに変更するだけでいいのですが、このidはglobローダーbaseからの相対パス(拡張子なし)であり、拡張子ありだった元のidと形式が異なるのでファイルパスとして扱っていた場合は注意が必要です。私のプロジェクトではエントリidを拡張子ありのファイルパスとして処理している部分で影響が出たので、同じく新しく追加されたfilePathフィールドに変更しました。
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
params: { slug: post.id },
props: post,
}));
}
render()関数の変更
エントリの.render()メソッドが廃止され、astro:contentからrender()をインポートして呼び出す形になりました。
import { getCollection, type CollectionEntry } from 'astro:content';
import { getCollection, render, type CollectionEntry } from 'astro:content';
// ...
const { Content } = await note.render();
const { Content } = await render(note);
tsconfig.jsonの更新
型定義の参照先がsrc/env.d.tsから、tsconfig.jsonのincludeで読む形に変わりました。これが今後のAstro推奨設定となり、後述するような手動での型定義を必要としない場合はもとのsrc/env.d.tsは削除しても問題ありません。
// tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
https://docs.astro.build/ja/guides/upgrade-to/v5/#changed-typescript-configuration
https://docs.astro.build/ja/guides/typescript/#tsconfig-templates
フォントパッケージの型定義追加
私のプロジェクトでは型定義を持たないフォントパッケージを使用しており、インポートしている部分で型エラーが出るようになったので明示的にsrc/env.d.tsに追加しました。たぶんtsconfig.jsonのincludeに追加した**/*が犯人。
// src/env.d.ts
declare module '@fontsource-variable/inter';
Astro v6への更新
実はv6への更新自体は非常に簡単で、ドキュメントを見る限り、v5のコンテンツコレクションの変更をサボらずにやっていれば対応箇所は多くないです。現に私のプロジェクトではv6で対応が必要なものは1つもなく、Astroと付随するアダプターのバージョンを上げるだけで済みました。曲者なのはVite 7への更新に伴う@tailwindcss/viteへの移行と、astrojs/cloudflare v13への更新に伴うCloudflare Pagesの廃止です。つまりはTailwindをv3からv4に、デプロイ先をPagesからWorkersに移行する必要があったということですね。
react-dom/serverエイリアスの削除
React 19+を使用しているプロジェクトがCloudflareにデプロイできない問題があり、下記の回避策を講じていたのですがv6で修正されていたのでこれを削除しました。
vite: {
resolve: {
// React+19をCFにデプロイ出来ない問題の回避
alias: import.meta.env.PROD ? {
"react-dom/server": "react-dom/server.edge",
} : undefined,
},
},
https://github.com/withastro/astro/issues/12824
@astrojs/tailwindから@tailwindcss/viteへの移行
Tailwind CSS v4でライブラリ自体が再設計され、Tailwind公式からVite用プラグイン(@tailwindcss/vite)が提供されるようになったのでAstroの公式統合(@astrojs/tailwind)はAstro v5時点で非推奨に。そしてAstro公式統合がvite 6依存だったので、7に上がったAstro v6でビルドが通らなくなりました。必然的にTailwind v4へ更新することになるので、tailwind.config.jsonや一部ユーティリティクラスの修正が必要です。更新にはマイグレーションツール(npx @tailwindcss/upgrade)もあるみたいです。
@astrojs/tailwind廃止
Tailwind CSS v4 Vite用プラグイン
@tailwindcss/viteへの移行
AstroのintegrationsからTailwindを取り除き、Viteのpluginsに追加します。
import tailwind from '@astrojs/tailwind';
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
integrations: [tailwind(), mdx(), react()],
integrations: [mdx(), react()],
vite: {
plugins: [
tailwindcss(),
ここで下記のエラーが発生。何やらトップレベルのViteとAstroが依存しているViteが競合しており、前者は@astrojs/cloudflareが要求している8.0.9のVite、後者はAstroの7.3.2のViteでした。cloudflareアダプターはViteの6,7,8を許容しているみたいなのでViteを7.xに上書きすることで解決。
型 'Plugin<any>[]' を型 'PluginOption' に割り当てることはできません。
プロパティ 'hotUpdate' の型に互換性がありません。
型 'import(".../node_modules/vite/...")' を
型 'import(".../node_modules/astro/node_modules/vite/...")' に割り当てることはできません
"pnpm": {
"overrides": {
"vite": "^7.3.1"
},
}
tailwind.config.jsの廃止とCSSへの移行
v4ではtailwind.config.jsが不要になり、設定をCSSファイルに直接記述するようになりました。@import "tailwindcss"でTailwindを取り込んだあと、各設定を@で始まるディレクティブで追加していきます。
/* src/styles/global.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (.dark &);
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@custom-variant mobile (@media (max-width: 800px));
スコープドCSSでの@applyの使用
Tailwind v4では、現時点でフレームワーク固有のスタイルブロックやCSSモジュール内での@apply、その他ユーティリティやバリアントがそのままでは機能しません。これらにアクセスする場合は@referenceで明示的にCSSを参照する必要があります。
<style>
@reference "../styles/global.css";
img {
@apply max-h-150 object-contain border border-black mobile:max-h-100;
}
</style>
https://tailwindcss.com/docs/upgrade-guide#using-apply-with-vue-svelte-or-css-modules
ユーティリティクラスの変更
詳しい変更箇所は割愛します。該当箇所で警告が出るはずなので、VSCodeならクイックフィックスでポチポチ変換していくか、AIに適当に変換してもらってください。
@astrojs/cloudflare v13への更新
CloudflareはAstroのホスティング先として有力な選択肢でしたが、Astroのローカル開発サーバーがNode.jsで動いているのでCloudflareの機能(ダッシュボードの設定やWorkersバインディング等)との相性が悪く、wrangler設定ファイルや.dev.varsファイルが使えないので開発用と本番用でコードを分ける必要があったりと、お世辞にも開発体験が良いものではありませんでした。一応、npx wrangler devコマンドでCloudflare用の設定ファイルを適用してビルド済みの成果物をプレビューすることは出来ましたが、Astroの開発サーバーで動いていないのでこれでテストしたものをそのままデプロイしたら本番で動かないみたいなことが容易に起こり得ました。v13ではAstroの開発サーバーがCloudflareのworkerdランタイム互換になったので、ローカル開発環境と本番環境で同じコードが動くようになり、開発体験が改善されました。
Astro.locals.runtime APIの廃止
CloudflareのAPIにアクセスする場合はAstro.locals.runtimeを使用していたのですが、v13でCloudflareのAPIがグローバルにアクセスできるようになったので、これを使用する必要がなくなりました。実行コンテキストはAstro.locals.cfContextで取得出来るようになっています。.dev.varsやwrangler.jsoncのvarsに定義した環境変数はwrangler typesコマンドで生成されるworker-configuration.d.tsにEnvインタフェースとして登録されるようになったので、envにインテリセンスが効いて型安全にアクセスできるようになったのが嬉しいところです。
import { env } from 'cloudflare:workers';
// ...
const customScheme = (Astro.locals as any).runtime.env.AUTHORIZATION_CALLBACK_CUSTOME_SCHEME;
const customScheme = env.AUTHORIZATION_CALLBACK_CUSTOME_SCHEME;
Cloudflare PagesからCloudflare Workersへの移行
astrojs/cloudflare v13のドキュメントをスクロールしていったら、見過ごせない一文が目に入り、どうやらv13からはCloudflare Pagesのサポートを切るみたいで、Workersへの移行が必要になりました。近年、Workersへの統合を進めていて、今後のPagesのサポートは限定的とのことなので早めの移行が推奨されます。Workersでの開発やビルド、デプロイに関する操作にはwrangler CLIを使用します。Githubと連携してCI/CDを実行してくれるWorkers Buildsを使えばwrangler CLIは必須ではないのですが、CI/CDワークフローが整うまではローカルで操作できたほうが何かと便利なので入れておいたほうがいいです。CLIで認証した後、後述するwrangler設定ファイルを準備してローカルでnpx wrangler deployコマンドを叩けば簡単にデプロイできます。
https://docs.astro.build/ja/guides/integrations-guide/cloudflare/#removed-cloudflare-pages-support
ビルド出力先の変更に伴うコードの修正
静的アセットのビルド出力先がdist/clientに変わるので、この変更に影響する部分は網羅的にチェックします。
writeBundle() {
// ビルド後にブランチ名付きファイルを削除
const distWellKnown = path.join(process.cwd(), 'dist', '.well-known');
const distWellKnown = path.join(process.cwd(), 'dist', 'client', '.well-known');
if (fs.existsSync(distWellKnown)) {
const files = fs.readdirSync(distWellKnown);
files.forEach(/** @param {string} file */ (file) => {
wrangler.jsoncの作成
Workersにデプロイするなら必須の設定ファイルです。厳密には、Astroではmain,compatibility_date,assetsの基本構成のみであれば不要でオプションみたいですが、私のプロジェクトでは環境ごとにWorkersプロジェクトを分ける必要があり、カスタム設定を記述するので作成します。アダプターを入れるかどうか、またアダプターによって変わると思いますが、Cloudflareの場合はdist/clientがビルド出力先になるのでdirectoryキーを指定します。not_found_handlingキーは、アセットが見つからなかった時の挙動を指定するもので、404-pageだと404ページを返し、single-page-applicationだとSec-Fetch-Mode: navigateのリクエストに対してはindex.htmlを、それ以外はWorkerに渡されるような挙動になります。今回のSSRのプロジェクトではプリレンダリングされたアセット以外はWorkerで処理する必要があるので”none”を指定します。(キー自体を設定しなくてもいい)
開発環境、本番環境で構成を分ける場合はenvセクションにそれぞれの環境の設定を記述しますが、一部の設定はトップレベル(envの外)に定義したものが各環境に継承されないので注意が必要です。継承されない設定に関しては各環境で個別に定義してください。また、このwrangler.jsoncはgit管理するものなので、ここに設定する環境変数は非機密なものに限られます。機密情報を設定する場合はwrangler secret putコマンドを使い、毎回手打ちするのも面倒なはずなのでGithub Secretsに保存しておき、デプロイ時にGithub Actionsを実行させるのが確実かつ楽だと思います。
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "sampleApp-development",
"main": "@astrojs/cloudflare/entrypoints/server",
"compatibility_date": "2026-04-22",
"assets": {
"directory": "./dist/client/",
"not_found_handling": "none"
},
"env": {
"development": {
"name": "sampleApp-development",
"vars": {
"ENVIRONMENT": "development",
}
},
"production": {
"name": "sampleApp",
"vars": {
"ENVIRONMENT": "production",
},
"routes": [{
"pattern": "sampleApp.com",
"custom_domain": true
}],
}
}
}
Wrangler Configuration
継承されるキー・されないキー
ビルドスクリプトの変更
環境の指定方法の変更
ここまでの対応でWorkersにデプロイできるかテストしてみたのですが、npx wrangler deploy —env production(まだ本番のカスタムドメインには接続していない状態)が動作しておらず、sample-appのWorkerが作られるのではなく、sample-app-developmentのワーカーに再デプロイするような挙動になっていました。.astroやdistを削除して再ビルドしてからデプロイしても同じで少しハマることに。
ドキュメントをよく見たらAstro 6.0から環境の指定方法が変わっていました。これまではビルドとデプロイが分かれていて、環境はデプロイ時に指定する形でしたが、Viteプラグインに移行したことで環境はビルド時に決まるようになったとのこと。完全に見落としていました!
Changed: Deploy to Cloudflare Environment
In Astro 5.x, you could build your Astro project once and deploy it to a specific Cloudflare environment with .wrangler deploy —env some-env
Since Astro 6.0, the integration relies on the Cloudflare Vite plugin and this behavior has changed. The environment is now determined during the build phase. Therefore, you must build your project separately for each environment.
To deploy to a specific Cloudflare environment, prefix your command with the variable. For example, the command will build your Astro project and deploy it with Wrangler using the environment.CLOUDFLARE_ENVCLOUDFLARE_ENV=some-env astro build && wrangler deploysome-env
Learn how to update your Cloudflare environments in the Migrate from wrangler dev guide.
変更:Cloudflare環境へのデプロイ
ということで、ビルド時にCLOUDFLARE_ENV={環境}を指定することで解決。また、ローカル(Windows)でもCI/CDでも同じコマンドで動くようにcross-envを付け、コマンドが長くなるのでpushコマンドにまとめます。
型定義ファイルの自動生成
wrangler typesコマンドを使うと、現状の構成に合わせてAPIの型定義ファイル(worker-configuration.d.ts)を自動生成してくれます。wrangler.jsoncに設定した環境変数などもこの型定義ファイルに反映されるようになるので、これをビルドスクリプトに組み込んでおくと便利です。
最終的にこうなりました↓
// package.json
"scripts": {
"dev": "wrangler types && pnpm copy-well-known && cross-env CLOUDFLARE_ENV=development astro dev",
"start": "wrangler types && astro dev",
"build": "wrangler types && pnpm copy-well-known && astro check && astro build",
"push": "pnpm push:development",
"push:development": "cross-env CLOUDFLARE_ENV=development pnpm build && pnpm exec wrangler deploy --env development",
"push:production": "cross-env CLOUDFLARE_ENV=production pnpm build && pnpm exec wrangler deploy --env production",
"astro": "astro",
"copy-well-known": "node scripts/copy-well-known.js"
},
GitHub Actionsでのデプロイ
機密情報はnpx wrangler secret putコマンドで設定し、デプロイと同時に登録することが出来ません。また、代わりにダッシュボードのシークレットで設定することは出来ますが、これは次回デプロイした時にwrangler.jsoncのvarsで上書きされてしまうので、デプロイのワークフロー内でwrangler secret putコマンドを実行するのが確実です。Cloudflareも各種設定はダッシュボードではなくWranglerやCI/CDパイプラインでの管理を推奨としているので、これがベストプラクティスのようです。
公式のwrangler/actionというものがあるみたいですが、ワークフロー自体シンプルですし、シークレットの設定をサポートしていないので自前で書きました。事前にCloudflareのAPIトークンとアカウントIDをGithub Secretsに保存し、各ステップで定義しておけば認証代わりになります。
name: Deploy
on:
push:
branches:
- main
- deploy
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
env:
DEPLOY_ENV: ${{ github.ref_name == 'deploy' && 'production' || 'development' }}
steps:
- name: チェックアウト
uses: actions/checkout@v6
- name: pnpmセットアップ
uses: pnpm/action-setup@v6
with:
version: latest
- name: Node.jsセットアップ
uses: actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
cache-dependency-path: "sample-app.com/pnpm-lock.yaml"
- name: 依存関係のインストール
run: pnpm install --frozen-lockfile
working-directory: sample-app.com
- name: Deploy
run: pnpm push:${{ env.DEPLOY_ENV }}
working-directory: sample-app.com
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: シークレットの設定
run: echo "$TURNSTILE_SECRET" | pnpm exec wrangler secret put TURNSTILE_SECRET --env ${{ env.DEPLOY_ENV }}
working-directory: sample-app.com
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TURNSTILE_SECRET: ${{ secrets.TURNSTILE_SECRET }}
Cloudflare AccessやTurnstile等の設定変更
pages.devドメインでこれらのサービスを使用している場合、当然ながら変更が必要です。
元のPagesプロジェクトの削除
デプロイされたサイトが問題なく動いていることと、カスタムドメインの繋ぎ直し等を実施したら元のCloudflare Pagesのプロジェクトは不要になるので消します。しかし、Pagesプロジェクトでのデプロイメント数が100を超えているとプロジェクトを削除できない問題があるらしく、公式ドキュメントに回避策が提示されています。Windowsでは下記のコマンドを叩いて全消しに成功。正確には一発で全部消えたわけではなく、途中で止まったので数回続けて叩いたら全部消せました。ページネーションの仕様なのかリクエスト制限なのか知りませんが。
npx wrangler pages deployment list --project-name sample-app --json | jq -r '.[].Id' | ForEach-Object { npx wrangler pages deployment delete $_ --project-name sample-app --force }
さいごに
型を管理するためにTSを使っているわけじゃ無いっつーの!