Block Alchemy rewrites existing WordPress content onto native block markup — in bulk from WP-CLI, or one page at a time from the block editor. Validated, backed up and reversible either way.
You point it at content that is already in the database — classic copy, a custom block, a repeating field group, a shortcode — and it rewrites that content as core blocks.
Where to start
- Writing a converter — the contract, the builders, and how to decline safely.
- Filter and action reference — every extension point, with what it receives and what it must return.
- Porting converters out of a theme — moving an existing conversion command into this plugin.
Two ways in
Both run the same conversion through the same code path (Core\Conversion), so the editor cannot convert anything the command would refuse, and neither can write a result that failed validation.
| WP-CLI | Block editor panel | |
|---|---|---|
| Scope | any number of posts | the post being edited |
| Dry run | the default; --write to persist | always, as a confirmation step |
| Refusals | grouped by frequency across the run | grouped by reason, in the dialog |
| Rollback | rollback --post=<ids> --write | a button, per post |
Bulk work belongs in WP-CLI. There is no bulk button and no background queue, and that is deliberate: a conversion's refusals are the product, and they are only worth anything if somebody reads them — which is exactly what a queued job cannot arrange.
The panel
It appears in the document sidebar, and only on a post that has convertible content or a conversion backup to restore. Everywhere else it is not loaded at all.
- It lists what it found, per converter, with counts.
- Convert to blocks… runs the conversion without writing, and shows what would change plus every refusal.
- Convert and save writes it, and the editor reloads.
- Roll back… restores the pre-conversion content and any field data the conversion removed.
Two constraints on it are load-bearing:
- The button is disabled while the post has unsaved changes. The conversion reads the stored post, server-side; anything held only in the editor is not part of that input, and the next save would write it straight back over the conversion.
- The editor reloads rather than patching blocks in place. That guarantees the editor matches the database, and keeps the conversion out of the undo stack — where one stray undo followed by a save would persist a half-reverted post.
Between the preview and the write, the panel returns a fingerprint of the content it was shown. If the stored post moved in the meantime — another tab, another user, an autosave — the write is refused rather than applying numbers nobody agreed to.
The rule everything follows from
Refuse rather than convert approximately. Every lossy case is a refusal with a reason, or a reported notice. Never a silent drop.
That rule is why the rest of it looks the way it does:
- Every conversion is validated before it goes near the database, and a failure blocks the write.
- Every write is read back and compared. On a mismatch the original is restored immediately.
- Every converted post keeps its pre-conversion content, plus a native revision.
- Conversion is idempotent. Running it twice converts nothing the second time.
What it converts
The engine is deliberately general. A converter declares where its data comes from:
| Source | Converter key is | Data comes from | Output |
|---|---|---|---|
post_content | a block name | the block's attributes | replaces the block in place |
post_meta | a layout name | a flattened meta key namespace | appended (or prepended) to the content |
shortcode | a shortcode tag | the tag's attributes and content | replaces the block holding it |
Out of the box it ships the converters that are the same on every site that needs them: classic content, and Genesis Blocks' Advanced Columns. Everything site-specific is yours to add, through a documented filter, in your own repository next to your own tests.
Genesis Blocks columns
genesis-blocks/gb-columns and the gb-column blocks inside it become core blocks, in the same shape Genesis saved: a core/group for its outer element, holding a core/columns for its inner grid, with a core/column for each column. Folding the two wrappers into one core/columns looks simpler and is not faithful — it picks up every margin core and the theme give .wp-block-columns, none of which reached Genesis' outer element.
- Layouts become column widths, taken from Genesis' stylesheet rather than its picker: its "75/25" layout renders as
2fr 1fr, and converts to two thirds and one third. The older picker's keys (one-columntosix-column) have no CSS of their own, and convert as equal columns. - A single column is not a row, and becomes a group with no
core/columnsinside — constrained to its maximum width when it has one, with the column's content in a group of its own, as Genesis nested a column inside its capped grid. That keeps it clear of a theme's styles for content in columns, and its gap, which one column never renders, is no reason to refuse. - Colours, padding, margin, alignment, the column gap, vertical alignment, the responsive toggle and the author's own classes all carry across, and so do visibility and the other preserved attributes, on every column. A column with a background, padding or margin gets a group inside it, because those belonged to an element Genesis sized to its content.
- A column's text alignment becomes core's
has-text-align-*class, and a centred block keeps itsaligncenterclass. Both are what core and the theme already style.
It refuses, and leaves the block exactly as it was, when:
- The theme would not render the gap. A gap other than Genesis' default needs block gap support:
settings.spacing.blockGapintheme.json, orappearanceTools: true, which turns it on. Without either,core/columnsshows its 2em whatever the block says. - There is a background image, or the block is floated left or right — core has no equivalent for either.
- The column count, the layout and the columns actually present disagree.
- An attribute, class or inline style is one it does not recognise — including markup saved by another version of Genesis Blocks, or decorated by another plugin.
Every conversion reports what will look different, and it is the same on every theme with block gap support: inside a core container, the theme's block gap replaces each block's own top margin, and the first and last block lose their outer margins, which Genesis kept. Core also stacks columns below 782px rather than Genesis' 600px. Open the first converted page at desktop and tablet width before converting in bulk.
Honest scope
This converts content, not code.
Conversion removes a plugin's hold on the content it rewrites, and that is a real removal: an acf/hero that becomes a core/group is rendered and edited by WordPress alone. There is no leftover editing interface to miss, because the block editor is the editing interface now.
What it does not do is remove the rest of that plugin's footprint — templates still calling get_field(), data stored outside post content, still-registered field groups, and the meta rows a post_meta conversion read but did not delete. Converted content no longer needs the plugin that produced it; whether the plugin can come off the site is a separate check.