EmDash CMS のフォームが表示されない原因と解決策

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...