A WP-CLI script that removes <strong> and <b> tags from inside h1–h5 headings in Gutenberg block editor posts.
When editors accidentally apply bold formatting to heading text in the Gutenberg block editor, WordPress wraps it in a <strong> tag:
<!-- Full heading bolded -->
<h2 class="wp-block-heading"><strong>Why We Chose DataHub</strong></h2>
<!-- Partial bold inside a heading -->
<h3 class="wp-block-heading">1. <strong>Quality</strong></h3>Headings are inherently bold by convention. The redundant <strong> tag adds unintended semantic weight and can interfere with screen readers and SEO. This script removes it cleanly without touching anything else in the post content.
- WP-CLI installed and accessible from the WordPress root directory
- PHP 7.4+
- The
DOMDocumentPHP extension (enabled by default on most hosts)
Open the script and set the constants at the top before running:
// Set to true to preview changes without writing anything to the database.
define( 'DRY_RUN', false );
// The post type slug to target.
define( 'TARGET_POST_TYPE', 'dh-stories' );
// Set to true to also strip <strong> and <b> tags that wrap only part of a
// heading, not just those wrapping the entire heading text.
define( 'PARTIAL_STRONG', false );When true, the script scans all posts and logs every heading it would change, but writes nothing to the database. Use this to review the scope of changes before committing.
The post type slug to target. Only published posts of this type are processed. Change this to reuse the script against other post types.
When false (default), only headings where <strong> or <b> is the sole child wrapping all text are affected:
<!-- Fixed when PARTIAL_STRONG is false or true -->
<h2><strong>Full Heading</strong></h2>
→ <h2>Full Heading</h2>When true, any <strong> or <b> tag found anywhere inside a heading is also removed, regardless of how much text it wraps:
<!-- Fixed only when PARTIAL_STRONG is true -->
<h3>1. <strong>Quality</strong></h3>
→ <h3>1. Quality</h3>Both modes run together when PARTIAL_STRONG is true.
Place the script in your WordPress root directory, then run from that location.
Dry run — preview what would change, no database writes:
# Set DRY_RUN to true in the script first, then:
wp eval-file fix-strong-headings.php --skip-pluginsCommit — apply changes to the database:
# Set DRY_RUN to false in the script, then:
wp eval-file fix-strong-headings.php --skip-pluginsThe --skip-plugins flag is required. Without it, deprecation notices or other output from plugins can be injected into the WP-CLI process and corrupt the content being written to the database.
--- DRY RUN --- No changes will be written.
Set DRY_RUN to false to apply changes.
PARTIAL_STRONG is enabled — partial bold inside headings will also be removed.
Found 25 published "dh-stories" posts. Scanning...
Post ID 5602 — "Netflix Streams Past Metadata Limitations"
[netflix] <h2> "Challenge"
[netflix] <h3> "1. Quality"
[netflix] <h3> "Impact"
--- Summary ---
Posts with changes found : 1
Headings fixed : 3
No changes were written. Set DRY_RUN to false to apply.
The script avoids passing the full post_content through DOMDocument, which would corrupt Gutenberg block comment delimiters and their JSON attributes — a particular problem with complex nested block structures from page builders like Kadence.
Instead it:
- Walks the raw
post_contentstring usingpreg_replace_callback, matching only<!-- wp:heading -->block segments - Passes only the small HTML fragment between the block delimiters to
DOMDocument - Checks whether the heading's sole child node is a
<strong>or<b>element and unwraps it - If
PARTIAL_STRONGis enabled, additionally finds and unwraps any remaining<strong>or<b>tags anywhere inside the heading - Reassembles the block with the original
<!-- wp:heading -->comment and attributes completely untouched - Writes the result directly via
$wpdb->update()rather thanwp_update_post(), bypassingwp_kses_post()and other content filters that strip inner block content from nested block structures
Change TARGET_POST_TYPE to the slug of any other post type and re-run. No other changes needed.
- Only published posts are processed. Drafts, scheduled posts, and trashed posts are skipped.
$wpdb->update()bypasses WordPress save hooks, so cache invalidation from plugins will not fire automatically. Flush your cache manually after running if your site uses a caching layer.- Always test on development and staging environments before running on production. Restore from a backup or post revision if anything looks wrong after a dry run.