EmDash CMS のフォームプラグインを使ってページ本文にフォームブロックを配置したところ、本番環境でフォームが表示されない問題が発生しました。
Cloudflare の Free プランだから動かないのではないか、Dynamic Workers が必要なのではないか、という疑いもありましたが、今回の原因はそこではありませんでした。
症状
管理画面ではフォームブロックをページ本文に追加できています。
対象ページの本文データにも、フォームブロックは保存されています。
しかし、公開ページを確認するとフォームが出力されません。
HTML 上ではフォーム本体が存在せず、本文レンダリング時にフォームブロックが落ちているような状態でした。
Cloudflare Free プランが原因ではなかった
最初に疑ったのは Cloudflare Workers のプラン制限です。
EmDash CMS の FAQ には、Free プランでは Dynamic Workers に依存するサンドボックス化されたプラグイン実行が使えない、という説明があります。
ただし、今回のフォーム表示は Dynamic Workers を使った隔離プラグイン実行ではありません。
確認したところ、フォーム定義 API は本番で正常に動作していました。
curl -X POST https://emdash-jp.com/_emdash/api/plugins/emdash-forms/definition \
-H "content-type: application/json" \
-d '{"formId":"01KR0VEPPREF0WEA8B8572GFV7"}' フォーム送信 API も正常に動作していました。
原因
原因は、Portable Text 内のフォームブロックを SSR でレンダリングする処理でした。
EmDash の PortableText は通常の本文ブロックをレンダリングできますが、フォームプラグインのカスタムブロックが本番 SSR の流れで期待通りに処理されていませんでした。
その結果、本文データにはフォームブロックが存在するのに、公開ページの HTML にはフォームが出力されない状態になっていました。
解決策
フォームブロックだけを明示的に検出し、D1 に保存されているフォーム定義を直接読んで SSR するコンポーネントを追加しました。
通常の本文ブロックは EmDash の PortableText に任せ、フォームブロックだけ専用コンポーネントで描画します。
構成は次のようにしました。
src/components/PortableTextWithForms.astro
src/components/FormEmbedDirect.astro PortableTextWithForms.astro では本文ブロックを走査し、emdash-form ブロックを見つけたら FormEmbedDirect に渡します。
---
import { PortableText } from "emdash/ui";
import FormEmbedDirect from "./FormEmbedDirect.astro";
const { value } = Astro.props;
const blocks = Array.isArray(value) ? value : [];
---
{
blocks.map((block) => {
const type = block?._type ?? block?.blockType;
if (type === "emdash-form") {
return <FormEmbedDirect node={block} />;
}
return <PortableText value={[block]} />;
})
} FormEmbedDirect.astro では Cloudflare の env.DB からフォーム定義を取得し、公開ページ用のフォーム HTML を SSR します。
---
import { env } from "cloudflare:workers";
const { node } = Astro.props;
const formId = node.formId ?? node.id;
const row = await env.DB.prepare(`
select data
from _plugin_storage
where plugin_id = 'emdash-forms'
and collection = 'forms'
and id = ?
limit 1
`).bind(formId).first();
const form = row ? JSON.parse(row.data) : null;
---
{form && (
<form class="ec-form" method="post" data-form-id={form.id}>
<input type="hidden" name="formId" value={form.id} />
{form.fields.map((field) => (
<label>
<span>{field.label}</span>
<input name={field.name} required={field.required} />
</label>
))}
<button type="submit">
{form.settings?.submitLabel ?? "Submit"}
</button>
</form>
)} 実際の実装では、textarea、honeypot、送信後メッセージ、フォーム送信用 JavaScript なども含めて出力しています。
ページ側の変更
固定ページと投稿ページでは、通常の PortableText の代わりに PortableTextWithForms を使います。
---
import PortableTextWithForms from "../../components/PortableTextWithForms.astro";
---
<PortableTextWithForms value={page.data.content} /> これにより、通常の本文ブロックはそのまま表示しつつ、フォームブロックだけ確実に SSR できるようになります。
まとめ
今回の問題は Cloudflare Free プランの制限ではありませんでした。
フォーム定義 API と送信 API は動作しており、問題は本文内のフォームブロックが公開ページの SSR で出力されないことでした。
対策として、Portable Text の中からフォームブロックを明示的に検出し、D1 のフォーム定義を直接読んで SSR することで、本番ページでもフォームを表示できるようになりました。
根本的には EmDash CMS 側、またはフォームプラグイン側で修正されるのが望ましい挙動です。サイト側では、今回のような専用ラッパーを用意することで回避できます。
なおこのバグは現在 issue が作成されており、将来的には修正が入る可能性が高いです。
https://github.com/emdash-cms/emdash/issues/154
本家に修正が入るのが待ち遠しいです。
Comments
Loading comments...