Block Alchemy

Last updatedView as Markdown

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

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-CLIBlock editor panel
Scopeany number of poststhe post being edited
Dry runthe default; --write to persistalways, as a confirmation step
Refusalsgrouped by frequency across the rungrouped by reason, in the dialog
Rollbackrollback --post=<ids> --writea 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.

  1. It lists what it found, per converter, with counts.
  2. Convert to blocks… runs the conversion without writing, and shows what would change plus every refusal.
  3. Convert and save writes it, and the editor reloads.
  4. 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:

SourceConverter key isData comes fromOutput
post_contenta block namethe block's attributesreplaces the block in place
post_metaa layout namea flattened meta key namespaceappended (or prepended) to the content
shortcodea shortcode tagthe tag's attributes and contentreplaces 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-column to six-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/columns inside — 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 its aligncenter class. 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.blockGap in theme.json, or appearanceTools: true, which turns it on. Without either, core/columns shows 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.

Have a question about this page?

Linchpin assistant

Was this helpful?