← All field notes
Blocks

Vibe-coding a Gutenberg block with an LLM in the loop

Blocks are where LLMs drift hardest — edit/save mismatches, missing block.json, hardcoded markup. Here's the loop that keeps them honest.

Custom blocks are the fastest way to feel an LLM's limits. The API surface is wide — block.json, registerBlockType, an edit component, a save component, attributes, supports — and models reliably get two of them subtly wrong. Vibe-coding a block works, but only if you drive the loop instead of accepting the first dump.

Start from block.json, always

The single most common failure is a model generating a block with registerBlockType in JS and no block.json, the metadata file that modern WordPress expects. Make the manifest the first artifact you demand — everything else hangs off it:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wpds/callout",
  "title": "Callout",
  "category": "text",
  "icon": "megaphone",
  "attributes": {
    "message": { "type": "string", "source": "html", "selector": "p" }
  },
  "editorScript": "file:./index.js"
}

The edit/save contract is where drift lives

In a static block, the markup your save function returns must match what's stored in post content byte-for-byte, or WordPress throws a block-validation error and offers to "attempt recovery." LLMs cause this constantly: they'll tweak the edit markup in one reply and forget to mirror it in save two replies later — a textbook case of the model losing track of a constraint it set itself. Keep both in view and change them together:

import { useBlockProps, RichText } from '@wordpress/block-editor';

export const save = ( { attributes } ) => {
    const blockProps = useBlockProps.save();
    return (
        <p { ...blockProps }>
            <RichText.Content value={ attributes.message } />
        </p>
    );
};

Drive the loop in small diffs

Don't ask for "a block with a color picker, an image, and a CTA" in one shot — you'll get a plausible blob with a broken save function. Ask for the manifest, then the edit component, then wire one attribute, then the next. Each small diff is less context for the model to hold, so it has less room to contradict itself. When it does drift, you'll see it in the block-validation warning immediately, not three features later. The block editor is unforgiving about mismatches — use that strictness as your test suite.

The takeaway: a block is a contract between edit, save, and block.json. Models are great at writing any one of the three and bad at keeping all three in sync. Your job is to hold the contract while the model fills it in.

Why models drift

Half of "the AI wrote bad code" is really the model losing track of your constraints mid-session. That failure mode has a name and a research home: Hidden State Drift ↗.

More field notes

SecurityThe security cost of AI-written plugins: the gaps models skipAI BehaviorHidden State Drift in your dev loop: why the model forgets your codebase mid-sessionWorkflowFrom chat to committed: a repeatable AI WordPress workflow