Change saved gutenberg blocks

When a post is saved the Gutenberg Blocks are rendered and it’s html is saved in the post_content in the database, most of the time. But what if you want to change the rendered html for all posts in every post.
Here we will explain how to change the html of saved Gutenberg blocks.

Add a class to all paragraph blocks

This use case is highly unlikely, but it is a simple one. So making it easier to explain.
So lets assume you would want to add a newer class to every paragraph on a single post on your site.

To start the block looks like this:

<!-- wp:paragraph -->
<p>To start the block looks like this:</p>
<!-- /wp:paragraph -->

With the added newer class it will look like this:

<!-- wp:paragraph {"className":"newer"} -->
<p class="newer">To start the block looks like this:</p>
<!-- /wp:paragraph -->

With the use case explained lets show the code to handle this.

changing the html of a block

The following code will change this html

<?php
$post                 = get_post( 898 ); // Get 1 post
$parsed_blocks        = parse_blocks( $post->post_content ); // parse all blocks in this post.
$updated_post_content = ''; // The updated post content will be inside here.

// Loop over the parsed blocks.
foreach ( $parsed_blocks as $block ) {
	if ( 'core/paragraph' !== $block['blockName'] ) {
		// only convert paragraphs, the rest we re-add untouched.
		$updated_post_content .= serialize_block( $block );
		continue;
	}

	// Load the innerHTML as an object so it's safer to manipulate.
	$dom = new DOMDocument;
	$dom->loadHTML( $block['innerHTML'], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );

	/** @var DOMElement $paragraph_node Get the <p> as a object */
	$paragraph_node = $dom->getElementsByTagName( 'p' )->item( 0 );
	// Get already set classes.
	$current_classes = $paragraph_node->getAttribute( 'class' );
	// Append `newer` class to the current list.
	$all_classes = trim( $current_classes . ' newer' );

	// Add the class attribute.
	$paragraph_node->setAttribute( 'class', $all_classes );
	$new_content = $dom->saveHTML();

	// Now put the new block together.
	$new_block = array(
		// We keep this the same.
		'blockName'    => 'core/paragraph',
		// also add the class as block attributes.
		'attrs'        => array( 'className' => $current_classes ),
		// I'm guessing this will come into play with group/columns, not sure.
		'innerBlocks'  => array(),
		// The actual content.
		'innerHTML'    => $new_content,
		// Like innerBlocks, I guess this will is used for groups/columns.
		'innerContent' => array( $new_content ),
	);

	$updated_post_content .= serialize_block( $new_block );
}

// Update the post with the content.
wp_update_post( array( 'ID' => $post->ID, 'post_content' => $updated_post_content, ) );

Run this code once and it will update this one post, ID 898 in this case. Pretty much every line has an explaining comment. But let’s go through the main steps.

First we get the post we want and convert to content to an array of blocks.
We loop over these blocks one by one.
As we only are changing the paragraphs we check and re-add other block types.

The paragraph blocks we load as a DomDocument. We only need to add or edit one attribute. you might be able to do this with a str_replace but DomDocument is more flexible and it can handle edge cases with more safety.

After that we check for existing classes. And add our newer class to the existing ones.
Even if there are no existing ones that’s fine.
Once the class is added we re-render the html content of the block.

The last thing we do is re-create an array that is the definition of a block. And render is as a html block.

Finally after all blocks are looped over we update the post.
You now updated all the Paragraph Gutenberg blocks in the post.

Change multiple posts

Now we covered updating a single post, we can pretty much wrap this in a loop to cover all posts.

<?php
$all_post_types = get_posts( array(
	'post_type'   => array( 'post', 'page' ), // add any posttype you would want to check.
	'post_status' => 'any', // all except trash.
	'nopaging'    => true, // no limits, get all posts.
) );

foreach ( $all_post_types as $post ) {
	// insert the code from the sample above
}

Closing thoughts

Keep in mind the final code you only need to run it once.
Before doing this I recommend creating a test post with a bunch of the same blocks but with all kind of options/settings tweaked. That should give you a good test case.

And of course create a backup before converting all posts and do deep check when it’s done.