Ultimately, I solved the problem by exporting the wp_posts table from the database and doing a regex search along the lines of
'([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]', '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9])', '(.*?)\[soundcloud url=\\"http://api\.soundcloud\.com/tracks/(.*?)\\" params=\\"auto_play=false&show_artwork=false&color=ff7700&show_playcount=false\\" width=\\"100%\\" height=\\"180\\" iframe=\\"true\\" /]
and replacing it with
'\1', '<!-- wp:paragraph -->\2<!-- /wp:paragraph --><!-- wp:embed \{"url":"http://api.soundcloud\.com/tracks/\3","type":"rich","providerNameSlug":"soundcloud","responsive":true\} --> <figure class="wp-block-embed is-type-rich is-provider-soundcloud wp-block-embed-soundcloud"><div class="wp-block-embed__wrapper"> http://api\.soundcloud\.com/tracks/\3 </div></figure> <!-- /wp:embed -->
Although this successfully converts each entire post (more than 5000) to two Gutenberg blocks, WordPress believes the paragraph block is damaged and needs to be recovered. To solve this, on a Github comment I found Javascript code for the edit admin page that will silently recover all supposedly damaged blocks when a post is opened for editing. I am including the code here (credit to AjXUthaya) in case the comment disappears.
admin.js:
import autoRecoverBlocks from './wordpress/autoRecoverBlocks';
// DECONSTRUCT: WP
const { wp = {} } = window || {};
const { domReady, data } = wp;
// AWAIT: jQuery to get ready
jQuery(document).ready(function ($) {
// DEFINE: Validation variables
const hasGutenbergClasses = $('body').hasClass('post-php') === true && $('.block-editor').length >= 1;
const gutenbergHasObject = domReady !== undefined && data !== undefined;
const gutenbergIsPresent = hasGutenbergClasses === true && gutenbergHasObject === true;
// IF: Gutenberg editor is present
if (gutenbergIsPresent === true) {
autoRecoverBlocks(false);
}
});
autoRecoverBlocks.js:
// FUNCTION: Recover block
const recoverBlock = (block = null, autoSave = false) => {
// DECONSTRUCT: WP object
const { wp = {} } = window || {};
const { data = {}, blocks = {} } = wp;
const { dispatch, select } = data;
const { createBlock } = blocks;
const { replaceBlock } = dispatch('core/block-editor');
const wpRecoverBlock = ({ name="", attributes = {}, innerBlocks = [] }) => createBlock(name, attributes, innerBlocks);
// DEFINE: Validation variables
const blockIsValid = block !== null
&& typeof block === 'object'
&& block.clientId !== null
&& typeof block.clientId === 'string';
// IF: Block is not valid
if (blockIsValid !== true) {
return false;
}
// GET: Block based on ID, to make sure it exists
const currentBlock = select('core/block-editor').getBlock(block.clientId);
// IF: Block was found
if (!currentBlock !== true) {
// DECONSTRUCT: Block
const {
clientId: blockId = '',
isValid: blockIsValid = true,
innerBlocks: blockInnerBlocks = [],
} = currentBlock;
// DEFINE: Validation variables
const blockInnerBlocksHasLength = blockInnerBlocks !== null
&& Array.isArray(blockInnerBlocks)
&& blockInnerBlocks.length >= 1;
// IF: Block is not valid
if (blockIsValid !== true) {
// DEFINE: New recovered block
const recoveredBlock = wpRecoverBlock(currentBlock);
// REPLACE: Broke block
replaceBlock(blockId, recoveredBlock);
// IF: Auto save post
if (autoSave === true) {
wp.data.dispatch("core/editor").savePost();
}
}
// IF: Inner blocks has length
if (blockInnerBlocksHasLength) {
blockInnerBlocks.forEach((innerBlock = {}) => {
recoverBlock(innerBlock, autoSave);
})
}
}
// RETURN
return false;
};
// FUNCTION: Attempt to recover broken blocks
const autoRecoverBlocks = (autoSave = false) => {
// DECONSTRUCT: WP object
const { wp = {} } = window || {};
const { domReady, data = {} } = wp;
const { select } = data;
// AWAIT: For dom to get ready
domReady(function () {
setTimeout(
function () {
// DEFINE: Basic variables
const blocksArray = select('core/block-editor').getBlocks();
const blocksArrayHasLength = Array.isArray(blocksArray)
&& blocksArray.length >= 1;
// IF: Blocks array has length
if (blocksArrayHasLength === true) {
blocksArray.forEach((element = {}) => {
recoverBlock(element, autoSave);
});
}
},
1
)
});
}
// EXPORT
export default autoRecoverBlocks;
To use the Javascript, save the two sections as two files. Put them in a /js/ directory in your activated template’s directory on your server. Then add an enqueuing function in your template’s functions.php file. My code is straightforward:
function add_script_to_menu_page()
{
// $pagenow, is a global variable referring to the filename of the current page,
// such as ‘admin.php’, ‘post-new.php’
global $pagenow;
if ($pagenow != 'post.php') {
return;
}
// loading js
wp_register_script( 'autoRecoverBlocks', get_template_directory_uri().'/js/autoRecoverBlocks.js', array('jquery-core'), false, true );
wp_enqueue_script( 'autoRecoverBlocks' );
}
add_action( 'admin_enqueue_scripts', 'add_script_to_menu_page' );
Additionally, for pages that do not have broken blocks but for which I do want to have the classic blocks converted to Gutenberg blocks, I have installed the Convert to Blocks plugin, which silently does the conversion when a post is saved.