Astroの下書きを、Cloudflare PagesのPRプレビューで確認する

draftをfalseにせず記事を確認できるように、Astroの出力を本番とPRプレビューで切り替えました。ビルド時の判定、RSSへの非掲載、noindex、実ビルドと公開URLでの検証を説明します。

TechHiraoku9 分で読めます

原稿とノートパソコンに表示したブログのプレビューを見比べる書き手を描いた、淡いティールグレー背景の黒ペン風イラスト

記事を書いていると、本文だけでは判断しにくいことがあります。見出しの間隔、コードブロックの幅、リンクカードの並び、アイキャッチとタイトルの組み合わせ。実際のサイトで見てから直したくなります。

このブログはAstroで作っていて、公開前の記事には draft: true を付けています。ところが、そのままPRを作ってCloudflare Pagesのプレビューを開いても、下書きの記事は404になりました。

記事を確認するために draft: false へ変えると、確認用の変更と公開の判断が混ざってしまいます。

そこで、記事は下書きのまま、PRのプレビューでだけ表示するようにしました。本番では出力せず、プレビューではサイトと同じレイアウトで読めます。今回は、その切り替えをどこで行い、何を検証したかをまとめます。

PRのプレビューにも、本番用のビルドが使われる

もともとの記事取得処理は、次の条件で下書きを除外していました。

const posts = await getCollection(
	'blog',
	({ data }) => import.meta.env.DEV || !data.draft,
);

npm run dev で起動する開発サーバーでは、下書きも表示します。通常のビルドでは、公開済みの記事だけを返します。

この条件はローカルでは便利でした。ただ、Cloudflare PagesのPRプレビューも、開発サーバーを動かしているわけではありません。このサイトでは npm run build で静的なHTMLを生成するため、PRプレビューでも下書きが除外されます。

プレビュー用のURLがあることと、下書きのHTMLが生成されることは別でした。

必要だったのは、開発中かどうかに加えて、どの環境へ出すビルドなのかを判定することでした。

公開するかどうかを、ビルド時に決める

今回のブログでは、Cloudflare Pagesの本番ブランチを main としています。Pagesがビルド時に渡す CF_PAGES と CF_PAGES_BRANCH を使い、main 以外なら下書きプレビューにします。

Cloudflareは、現在のデプロイ先URLを表す CF_PAGES_URL も提供しています。これは後述するcanonicalやOGPに使います。Cloudflare Pagesのビルド時環境変数

環境の判定は、記事の取得処理に直接書かず、src/lib/build-settings.mjs にまとめました。

export function getBuildSettings(env = process.env) {
	const onPages = env.CF_PAGES === '1';
	const branch = env.CF_PAGES_BRANCH?.trim();
	const draftPreview = onPages
		? Boolean(branch && branch !== 'main')
		: env.DRAFT_PREVIEW === '1';
	const site = (draftPreview && onPages && env.CF_PAGES_URL)
		|| env.SITE_URL
		|| 'https://hiraoku.dev';
	return { draftPreview, site };
}

ここで意識したのは、判定できないときに下書きを出さないことです。

Pages上なのにブランチ名が空なら、プレビューとして扱いません。また、Pagesの main では、ローカル確認用の DRAFT_PREVIEW=1 が設定されていても下書きを表示しません。Pagesの判定を先に行うため、ローカル用の指定で本番の条件を上書きできません。

main が本番という条件は、このサイトの設定であり、Cloudflare全体の決まりではありません。本番ブランチを変更するときは、この判定も変える必要があります。

記事の取得条件を共通化する

判定した値は、Astroの設定からViteの define でコードへ埋め込みます。src/lib/preview.ts がその値を読み、記事を取得する処理から使います。

以下は astro.config.mjs の関係する部分です。

const { draftPreview, site } = getBuildSettings();

export default defineConfig({
	site,
	integrations: [mdx(), ...(draftPreview ? [] : [sitemap()])],
	vite: {
		define: {
			'import.meta.env.PUBLIC_DRAFT_PREVIEW':
				JSON.stringify(String(draftPreview)),
		},
	},
	// Markdownなど、既存の設定は省略
});
// src/lib/preview.ts
export const isDraftPreview =
	import.meta.env.PUBLIC_DRAFT_PREVIEW === 'true';

この値はビルド時に固定します。ブラウザーのクエリパラメーターで下書きを表示する方式ではありません。本番で下書きのHTMLを生成しなければ、URLに何かを足しても記事は出てきません。

記事取得の入口は、次のように変更しました。

export async function getPosts({
	includeDrafts = import.meta.env.DEV || isDraftPreview,
} = {}): Promise<Post[]> {
	const posts = await getCollection(
		'blog',
		({ data }) => includeDrafts || !data.draft,
	);
	return posts.sort(
		(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
	);
}

このブログでは、記事ページの getStaticPaths()、記事一覧、タグ一覧などが getPosts() を使っています。共通の取得条件を変えることで、プレビューでは下書きのページだけでなく、そこへ移動するための一覧も生成できます。

実際にこの方法を使うなら、記事の取得経路が共通化されているかを先に確認しておくとよいでしょう。一覧だけで除外しても、記事ページを別の処理で生成していれば、そのページが残る可能性があります。

読める場所と、配信する場所を分ける

プレビューで下書きを読めるようにしても、RSSへ配信する必要はありません。

このサイトのRSS生成では、環境にかかわらず下書きを除外するようにしました。

const posts = await getPosts({ includeDrafts: false });

サイトマップは、プレビューでは生成しません。前のAstro設定で、draftPreview のときに sitemap() を有効にしていないのはこのためです。これは下書きだけを取り除く方式ではなく、プレビュー全体のサイトマップを省く方式です。

canonicalとOGPのURLには、プレビューの CF_PAGES_URL を使います。本番ではまだ存在しない記事について、本番ドメインのURLを出力しないようにしました。通常のビルドでは、これまでどおり本番ドメインを使います。

画面にも違いを表示します。プレビュー全体には「下書きプレビュー」、下書きの記事と一覧のカードには「下書き」を付けました。別タブで本番とプレビューを開いていても、どちらを見ているか判断できます。

noindexは、閲覧を制限する仕組みではない

今回の確認用URLは、URLを知っている人が閲覧できる形にしました。自分で確認するだけでなく、必要なら誰かに見てもらうこともできます。

ただし、本番に載せないことと、他人が読めないことは違います。

プレビューのHTMLには、次のmetaタグを出力します。

<meta name="robots" content="noindex, nofollow" />

Cloudflare Pagesも、プレビューに X-Robots-Tag: noindex ヘッダーを付けます。このヘッダーは、今回の実際のプレビューURLでも確認できました。Cloudflare Pagesのプレビューと検索除外

ここで一度、設定を見直しました。当初は robots.txt でも巡回を拒否していましたが、巡回を拒否すると検索エンジンがページを取得できず、HTMLやヘッダーの noindex を読めない場合があります。Googleも、noindex を読ませるにはクロールを許可する必要があると説明しています。Googleのnoindexの説明

そのため、プレビューの robots.txt は巡回を許可し、サイトマップの場所は記載しない構成に直しました。

User-agent: *
Allow: /

noindex は検索エンジンへの指示であり、認証ではありません。検索結果からの除外をその場で確認できるものでもありません。今回確認したのは、metaタグとHTTPヘッダーが出力されていることです。

Cloudflare PagesのデプロイごとのURLは、後からも参照できます。下書きを修正したりブランチをマージしたりしても、過去のプレビューが自動で秘密になるわけではありません。機密情報を扱うなら、Cloudflare Accessなどで閲覧者を制限する必要があります。プレビューのアクセス制御とURL

判定のテストだけで終わらせない

環境変数の判定には、テスト用のオブジェクトを渡せるようにしました。process.env を直接書き換えず、通常ビルド、Pagesのブランチ、本番でのローカル用指定、ブランチ情報の欠落などを確かめます。

ただ、それだけでは実際のHTMLが出力されるか分かりません。設定値が正しくても、記事取得やルーティングのどこかで使い忘れる可能性があります。

そこで、一時的な draft: true の記事を作り、実際にビルドする検証も用意しました。

ビルド条件確認する結果
通常のビルド下書きのページと一覧のリンクがなく、RSS・サイトマップにも載らない
PagesのPRプレビュー下書きのページと一覧を生成し、案内表示・noindex・プレビューのcanonicalを含む。RSSには載らず、サイトマップも生成しない
Pagesの main に DRAFT_PREVIEW=1 を指定ローカル用の指定があっても、下書きのページを生成しない

この検証は npm run test:previews で実行できます。確認用の記事は、検証の最後に削除します。同じ出力先を使うため、別のビルドと並行して動かさないでください。

さらに、Pagesへデプロイした後のURLも確認しました。MCP記事は下書きのままプレビューでHTTP 200となり、画面に下書きの案内が出ました。同じ記事の本番URLはHTTP 404でした。RSSへの非掲載と、canonicalがプレビューを指すことも確認しています。

ローカルのテスト、生成したHTML、デプロイしたURLは、それぞれ確認する対象が違います。公開の判断に関わる処理なので、この3つを分けて確認しました。

公開フラグを変えるのは、公開するときだけ

普段の記事確認では、draft: true のままPRを作ります。PRの Cloudflare Pages → Details からプレビューURLを開き、記事の見た目を確認します。記事を修正して同じブランチへpushすれば、そのブランチのプレビューも更新されます。

ローカルでビルド結果を確かめたい場合は、次のコマンドを使えます。

npm run build:preview
npm run preview

通常の出力へ戻すときは npm run build を実行します。公開すると決めた段階で、記事の draft を false に変えます。

今回分けたかったのは、記事を確認する作業と、公開する判断でした。下書きを見られる場所を別に用意すると、公開フラグを切り替えずに、実際のレイアウトを見ながら推敲できます。

ほかの記事も読む