Skip to content

Brian B. Tung

[Emdash] How an Unchecked Empty Submit and a Falsy-ID Guard in convertPTBlock Created a Permanent Autosave-Erasure Loop for YouTube Embeds

How an Unchecked Empty Submit and a Falsy-ID Guard in convertPTBlock Created a Permanent Autosave-Erasure Loop for YouTube Embeds

I spent a while debugging why YouTube blocks would disappear from my blog posts. I’d type `/youtube` in the EmDash editor, paste a URL, save — and after the page reloaded, the embed was gone. Not just from the rendered page; gone from the editor too, as if I’d never added it.

The database showed no `_type: "youtube"` block anywhere. This details the bug, tracing the full chain of failure, and the fix.

The Setup

My site uses EmDash CMS on Astro 6 / Cloudflare Workers with `@emdash-cms/plugin-embeds`. Blog post bodies are stored as Portable Text — a structured JSON array where each block has a `_type` and `_key`.

{ "_type": "youtube", "_key": "abc123", "id": "https://youtube.com/watch?v=..." }

The editor is TipTap (ProseMirror-based), and EmDash translates between ProseMirror nodes and Portable Text using `portableTextToProsemirror` on load and `prosemirrorToPortableText` on save.

What Was Actually Happening

The failure involved four cooperating bugs that formed a self-healing-erasure loop.

Bug 1 — The Modal Always Inserts, Even Empty

const handleSubmit = (e) => {
    e.preventDefault();
    if (block?.fields && block.fields.length > 0) onInsert(formValues);
    else {
        const url = typeof formValues.id === "string" ? formValues.id.trim() : "";
        if (url) onInsert({ id: url });
    }
};

For field-based blocks like YouTube, `onInsert(formValues)` ran unconditionally. Pressing Enter on a blank modal inserted a YouTube block with `id: undefined`, which became `id: ""` in the saved Portable Text.

{ "_type": "youtube", "_key": "abc123", "id": "" }

Bug 2 — The Converter Drops Blocks With Empty IDs

default: {
    const { _type, _key, id, url, ...rest } = block;
    const data = Object.fromEntries(
        Object.entries(rest).filter(([k]) => !k.startsWith("_"))
    );
    const hasFieldData = Object.keys(data).length > 0;
    if (id || url || hasFieldData) return {
        type: "pluginBlock",
        attrs: { blockType: _type, id: id || url || "", data }
    };

    return {
        type: "paragraph",
        content: [{
            type: "text",
            text: `[Unknown block type: ${block._type}]`,
            marks: [{ type: "code" }]
        }]
    };
}

Because `id`, `url`, and `hasFieldData` were all falsy for the empty YouTube block, the converter silently downgraded it into a paragraph containing `[Unknown block type: youtube]`.

Bug 3 — The Editor Overwrites Form State

React.useEffect(() => {
    if (item) {
        setFormData(item.data);
    }
}, [item?.updatedAt, itemDataString]);

After the page refetched the saved item, the corrupted body replaced the editor’s in-memory form state.

Bug 4 — Autosave Seals the Fate

EmDash autosaved two seconds later, permanently overwriting the original embed block with the corrupted paragraph node.

The Full Loop

User types /youtube, presses Enter on blank modal
        ↓
Bug 1: handleSubmit calls onInsert({}) unconditionally
        ↓
Editor shows YouTube widget with id=""
        ↓
User saves → { _type: "youtube", id: "" } stored
        ↓
Bug 2: convertPTBlock converts it into [Unknown block type: youtube]
        ↓
Bug 3: useEffect overwrites formData with corrupted content
        ↓
Bug 4: Autosave persists corrupted content
        ↓
YouTube embed is gone forever

The Fix

Fix 1 — Always Preserve Plugin Blocks

default: {
    const { _type, _key, id, url, ...rest } = block;
    const data = Object.fromEntries(
        Object.entries(rest).filter(([k]) => !k.startsWith("_"))
    );

    return {
        type: "pluginBlock",
        attrs: { blockType: _type, id: id || url || "", data }
    };
}

Removing the falsy guard ensures plugin blocks always round-trip faithfully, even when fields are empty.

Fix 2 — Guard the Modal Submit

if (block?.fields && block.fields.length > 0) {
    if (hasPluginBlockFormData(formValues)) onInsert(formValues);
}

This mirrors the existing validation logic and prevents blank YouTube blocks from being inserted at all.

Why Both Fixes Matter

Fix 2 prevents invalid blocks from being created, but Fix 1 is still necessary because previously-saved empty blocks — or any future malformed plugin blocks — must survive serialization without being destroyed.

Patching a Third-Party Package

The fix lives in `patches/@emdash-cms+admin+0.10.0.patch` via `patch-package`. Because `patch-package` 8.0.1 doesn’t auto-detect `pnpm-lock.yaml`, the patch hunks had to be constructed manually using `diff -u`.

rm -rf node_modules/.vite dist
pnpm dev:ui

Clearing `node_modules/.vite` is required because the admin bundle is cached inside Vite’s SSR optimizer output. Without clearing the cache, the worker continues serving the stale patched bundle.